chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Source
|
||||
src/
|
||||
|
||||
# Config files
|
||||
tsconfig.json
|
||||
.biome*
|
||||
biome.json
|
||||
|
||||
# Development files
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,69 @@
|
||||
# @react-scan/vite-plugin-react-scan
|
||||
|
||||
## 0.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.7
|
||||
|
||||
## 0.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.6
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.5
|
||||
|
||||
## 0.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- lite
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.4
|
||||
|
||||
## 0.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.3
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.2
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: infinite mounting
|
||||
- Updated dependencies
|
||||
- react-scan@0.5.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cleanup
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- Updated dependencies [9d38ffe]
|
||||
- react-scan@0.5.0
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2025 Aiden Bai, Million Software, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,78 @@
|
||||
# @react-scan/vite-plugin-react-scan
|
||||
|
||||
A Vite plugin that integrates React Scan into your Vite application, automatically detecting performance issues in your React components.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install -D @react-scan/vite-plugin-react-scan react-scan
|
||||
|
||||
# pnpm
|
||||
pnpm add -D @react-scan/vite-plugin-react-scan react-scan
|
||||
|
||||
# yarn
|
||||
yarn add -D @react-scan/vite-plugin-react-scan react-scan
|
||||
```
|
||||
|
||||
> **Note:** Make sure `react-scan` is installed as a peer dependency. The plugin will automatically locate it in your project's dependency tree.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the plugin to your `vite.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import reactScan from '@react-scan/vite-plugin-react-scan';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
reactScan({
|
||||
// options (optional)
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `enable` | `boolean` | `process.env.NODE_ENV === 'development'` | Enable/disable scanning |
|
||||
| `scanOptions` | `object` | `{ ... }` | Custom React Scan options |
|
||||
| `autoDisplayNames` | `boolean` | `false` | Automatically add display names to React components |
|
||||
| `debug` | `boolean` | `false` | Enable debug logging |
|
||||
|
||||
## Example Configuration
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import reactScan from '@react-scan/vite-plugin-react-scan';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
reactScan({
|
||||
enable: true,
|
||||
autoDisplayNames: true,
|
||||
scanOptions: {} // React Scan specific options
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Development vs Production
|
||||
|
||||
- In development: The plugin injects React Scan directly into your application for real-time analysis
|
||||
- In production: The plugin can be disabled/enabled by default with specific options
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
||||
|
||||
## License
|
||||
|
||||
React Scan Vite Plugin is [MIT-licensed](LICENSE) open-source software by Aiden Bai, [Million Software, Inc.](https://million.dev), and [contributors](https://github.com/aidenybai/react-scan/graphs/contributors).
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@react-scan/vite-plugin-react-scan",
|
||||
"version": "0.2.7",
|
||||
"description": "A Vite plugin for React Scan - detects performance issues in your React app.",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && NODE_ENV=production vp pack",
|
||||
"dev": "vp pack --watch",
|
||||
"lint": "vp lint",
|
||||
"format": "vp fmt",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-scan": {
|
||||
"optional": false
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-scan",
|
||||
"react scan",
|
||||
"render",
|
||||
"performance",
|
||||
"vite",
|
||||
"vite plugin"
|
||||
],
|
||||
"author": {
|
||||
"name": "Team React Scan",
|
||||
"url": "https://github.com/aidenybai/react-scan"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/aidenybai/react-scan",
|
||||
"bugs": {
|
||||
"url": "https://github.com/aidenybai/react-scan/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aidenybai/react-scan.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.25.9",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"babel-plugin-add-react-displayname": "^0.0.5",
|
||||
"cheerio": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-scan": ">=0.5.7",
|
||||
"vite": "^2 || ^3 || ^4 || ^5 || ^6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/cheerio": "^1.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
declare module 'babel-plugin-add-react-displayname/index.js';
|
||||
@@ -0,0 +1,321 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { transformAsync } from '@babel/core';
|
||||
import babelPluginReactDisplayName from 'babel-plugin-add-react-displayname/index.js';
|
||||
import * as cheerio from 'cheerio';
|
||||
import type { Options } from 'react-scan';
|
||||
import type { Plugin, ResolvedConfig } from 'vite';
|
||||
|
||||
async function resolveModuleFileContent(moduleName: string, startDir: string = process.cwd()) {
|
||||
const modulePath = path.join('node_modules', moduleName);
|
||||
const resolvedPath = path.resolve(startDir, modulePath);
|
||||
|
||||
try {
|
||||
return await fs.promises.readFile(resolvedPath, 'utf-8');
|
||||
} catch {
|
||||
const parentDir = path.dirname(startDir);
|
||||
if (parentDir !== startDir) {
|
||||
return resolveModuleFileContent(moduleName, parentDir);
|
||||
}
|
||||
throw new Error(`Module ${moduleName} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
debug: (...args: unknown[]) => void;
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
const createLogger = (prefix: string, debug = false): Logger => {
|
||||
return {
|
||||
debug: (...args: unknown[]) =>
|
||||
debug && process.stdout.write(`[${prefix}] ${args.join(' ')}\n`),
|
||||
info: (...args: unknown[]) =>
|
||||
process.stdout.write(`[${prefix}] ${args.join(' ')}\n`),
|
||||
warn: (...args: unknown[]) =>
|
||||
process.stderr.write(`[${prefix}] WARN: ${args.join(' ')}\n`),
|
||||
error: (...args: unknown[]) =>
|
||||
process.stderr.write(`[${prefix}] ERROR: ${args.join(' ')}\n`),
|
||||
};
|
||||
};
|
||||
|
||||
interface ReactScanPluginOptions {
|
||||
/**
|
||||
* Enable/disable scanning
|
||||
* @default process.env.NODE_ENV === 'development'
|
||||
*/
|
||||
enable?: boolean;
|
||||
|
||||
/**
|
||||
* Custom React Scan options
|
||||
*/
|
||||
scanOptions?: Options;
|
||||
|
||||
/**
|
||||
* Enable debug logging
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* Automatically add display names to React components
|
||||
* @default true
|
||||
*/
|
||||
autoDisplayNames?: boolean;
|
||||
}
|
||||
|
||||
const PLUGIN_NAME = 'vite-plugin-react-scan';
|
||||
|
||||
const DEFAULT_SCAN_OPTIONS: Partial<Options> = {};
|
||||
|
||||
const validateOptions = (options: ReactScanPluginOptions) => {
|
||||
if (options.scanOptions && typeof options.scanOptions !== 'object') {
|
||||
throw new Error('scanOptions must be an object');
|
||||
}
|
||||
|
||||
if (options.enable !== undefined && typeof options.enable !== 'boolean') {
|
||||
throw new Error('enable must be a boolean');
|
||||
}
|
||||
|
||||
if (options.debug !== undefined && typeof options.debug !== 'boolean') {
|
||||
throw new Error('debug must be a boolean');
|
||||
}
|
||||
|
||||
if (
|
||||
options.autoDisplayNames !== undefined &&
|
||||
typeof options.autoDisplayNames !== 'boolean'
|
||||
) {
|
||||
throw new Error('autoDisplayNames must be a boolean');
|
||||
}
|
||||
};
|
||||
|
||||
const JSX_EXTENSIONS = ['.jsx', '.tsx'] as const;
|
||||
const REACT_SCAN_IDENTIFIER = 'react-scan';
|
||||
|
||||
const isJsxFile = (id: string) =>
|
||||
JSX_EXTENSIONS.some((ext) => id.endsWith(ext));
|
||||
|
||||
const reactScanPlugin = (options: ReactScanPluginOptions = {}): Plugin => {
|
||||
validateOptions(options);
|
||||
const {
|
||||
enable = process.env.NODE_ENV === 'development',
|
||||
scanOptions = DEFAULT_SCAN_OPTIONS,
|
||||
debug = false,
|
||||
autoDisplayNames = false,
|
||||
} = options;
|
||||
|
||||
let config: ResolvedConfig;
|
||||
let isBuild = false;
|
||||
let scanFilePath = '';
|
||||
let assetsDir = '';
|
||||
|
||||
const log = createLogger(PLUGIN_NAME, debug);
|
||||
|
||||
const generateScanScript = (options: Options = {}) => {
|
||||
const hasOptions = Object.keys(options).length > 0;
|
||||
|
||||
if (isBuild) {
|
||||
// Create a proper JSON string for the options and wrap it in single quotes
|
||||
const optionsJson = hasOptions ? `${JSON.stringify(options)}` : '{}';
|
||||
|
||||
return `
|
||||
<script>
|
||||
const runScan = (options) => {
|
||||
if (reactScan){
|
||||
reactScan(${optionsJson});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="${scanFilePath}" onload="runScan()"></script>
|
||||
`;
|
||||
}
|
||||
|
||||
// Development version - use the base path from config
|
||||
const base = config.base || '/';
|
||||
return `
|
||||
<script type="module">
|
||||
import { scan } from '${base}@id/react-scan';
|
||||
(async () => {
|
||||
try {
|
||||
scan(${hasOptions ? JSON.stringify(options) : ''});
|
||||
} catch (error) {
|
||||
console.error('[${PLUGIN_NAME}] Scan failed:', error);
|
||||
}
|
||||
})();
|
||||
</script>`;
|
||||
};
|
||||
|
||||
return {
|
||||
name: PLUGIN_NAME,
|
||||
enforce: 'pre',
|
||||
|
||||
config(config) {
|
||||
return {
|
||||
optimizeDeps: {
|
||||
exclude: [...(config.optimizeDeps?.exclude || []), 'react-scan'],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
transform: async (code, id) => {
|
||||
if (!autoDisplayNames || !isJsxFile(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await transformAsync(code, {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
isTSX: isJsxFile(id),
|
||||
allExtensions: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
},
|
||||
],
|
||||
babelPluginReactDisplayName,
|
||||
],
|
||||
filename: id,
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
});
|
||||
|
||||
if (!result?.code) {
|
||||
log.warn(`No code generated for ${id}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug(`Successfully transformed ${id}`);
|
||||
return { code: result.code };
|
||||
} catch (error) {
|
||||
log.error(`Failed to transform ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
isBuild = config.command === 'build';
|
||||
assetsDir = config.build?.assetsDir || 'assets';
|
||||
const base = config.base || '/';
|
||||
|
||||
// Ensure base path is properly formatted
|
||||
scanFilePath = path.posix.join(base, assetsDir, 'auto.global.js');
|
||||
|
||||
log.debug('Plugin initialized with config:', {
|
||||
mode: config.mode,
|
||||
base,
|
||||
enable,
|
||||
isBuild,
|
||||
assetsDir,
|
||||
scanOptions,
|
||||
scanFilePath,
|
||||
});
|
||||
},
|
||||
|
||||
transformIndexHtml(html) {
|
||||
if (!enable) {
|
||||
log.debug('Plugin disabled');
|
||||
return html;
|
||||
}
|
||||
|
||||
try {
|
||||
const $ = cheerio.load(html);
|
||||
const scanScript = generateScanScript(scanOptions);
|
||||
|
||||
// Remove any existing React Scan script to avoid duplicates
|
||||
let removedCount = 0;
|
||||
$('script').each((_index, element) => {
|
||||
const content = $(element).html() || '';
|
||||
if (content.includes(REACT_SCAN_IDENTIFIER)) {
|
||||
$(element).remove();
|
||||
removedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (removedCount > 0) {
|
||||
log.debug(`Removed ${removedCount} existing scan script(s)`);
|
||||
}
|
||||
|
||||
if (isBuild) {
|
||||
// In build, insert at the beginning of head
|
||||
$('head').prepend(scanScript);
|
||||
log.debug(
|
||||
'Injected scan script at the beginning of head (build)',
|
||||
);
|
||||
} else {
|
||||
// In development, insert after Vite's client script
|
||||
const viteClientScript = $('script[src="/@vite/client"]');
|
||||
if (viteClientScript.length) {
|
||||
viteClientScript.after(scanScript);
|
||||
log.debug('Injected scan script after Vite client (serve)');
|
||||
} else {
|
||||
$('head').append(scanScript);
|
||||
log.debug('Injected scan script at end of head (serve)');
|
||||
}
|
||||
}
|
||||
|
||||
return $.html();
|
||||
} catch (error) {
|
||||
log.error('Failed to transform HTML:', error);
|
||||
return html;
|
||||
}
|
||||
},
|
||||
|
||||
resolveId(id) {
|
||||
if (!isBuild && id === `/@id/${REACT_SCAN_IDENTIFIER}`) {
|
||||
log.debug('Resolving react-scan module');
|
||||
return REACT_SCAN_IDENTIFIER;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async generateBundle() {
|
||||
if (isBuild && enable) {
|
||||
log.debug('Build started, processing react-scan');
|
||||
|
||||
try {
|
||||
const moduleNamePath = `${REACT_SCAN_IDENTIFIER}/dist/auto.global.js`;
|
||||
const content = await resolveModuleFileContent(moduleNamePath);
|
||||
|
||||
// Let Vite handle the file placement in configured assets directory
|
||||
const assetFileName = `${assetsDir}/auto.global.js`;
|
||||
|
||||
// Emit the file to the build output
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: assetFileName,
|
||||
source: content,
|
||||
});
|
||||
|
||||
// Store the full path for use in the script tag
|
||||
scanFilePath = `/${assetFileName}`;
|
||||
log.debug('Emitted react-scan as asset:', assetFileName);
|
||||
} catch (error) {
|
||||
log.error('Failed to process react-scan:', error);
|
||||
throw new Error(
|
||||
`Unable to locate '${REACT_SCAN_IDENTIFIER}'. This module is a required peer dependency.
|
||||
Please ensure 'react-scan' is installed in your project using your preferred package manager.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildEnd() {
|
||||
if (isBuild) {
|
||||
log.debug('Build completed');
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default reactScanPlugin;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"isolatedModules": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
entry: ["./src/index.ts"],
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
platform: "node",
|
||||
// ESM stays .js (package.json has "type": "module"); CJS gets .cjs
|
||||
fixedExtension: false,
|
||||
minify: process.env.NODE_ENV === "production",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user