Skip to content

Commit e2e28da

Browse files
authored
Merge pull request #2 from BAJ-/npm-package
Npm package
2 parents 193058e + 6ad85d3 commit e2e28da

64 files changed

Lines changed: 1142 additions & 265 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: '22'
18+
cache: 'npm'
19+
registry-url: 'https://registry.npmjs.org'
20+
- run: npm ci
21+
- run: npm run typecheck
22+
- run: npm run lint
23+
- run: npm test
24+
- run: npm audit --omit=dev
25+
- run: npm pack --dry-run
26+
- run: npm publish --provenance --access public
27+
env:
28+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Bjørn A. Johansen
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,80 @@
1-
# React Observatory
1+
# Reactoscope
2+
3+
Explore, stress-test, and get AI feedback on your React components — without writing a single test or storybook file.
4+
5+
## Features
6+
7+
- **Explore** — Renders any component with auto-generated prop controls based on its TypeScript types
8+
- **Stress Test** — Measures render performance, detects non-determinism, and spots memory leaks via server-side rendering
9+
- **AI Feedback** — Connects to a local Ollama instance to review component source code and provide suggestions
10+
11+
## Quick Start
12+
13+
### As a standalone CLI (no Vite project required)
14+
15+
```bash
16+
npx reactoscope path/to/MyComponent.tsx
17+
```
18+
19+
This starts a dev server and opens the Observatory UI in your browser.
20+
21+
### As a Vite plugin
22+
23+
```bash
24+
npm install reactoscope --save-dev
25+
```
26+
27+
Add it to your Vite config:
28+
29+
```ts
30+
import { defineConfig } from 'vite'
31+
import react from '@vitejs/plugin-react'
32+
import { observatory } from 'reactoscope'
33+
34+
export default defineConfig({
35+
plugins: [react(), ...observatory()],
36+
})
37+
```
38+
39+
Then visit `/__observatory?component=path/to/MyComponent.tsx` in your browser while the dev server is running.
40+
41+
## Options
42+
43+
```ts
44+
observatory({
45+
ollamaUrl: 'http://localhost:11434', // default
46+
})
47+
```
48+
49+
| Option | Type | Default | Description |
50+
| ----------- | -------- | ------------------------ | --------------------------------------------------------- |
51+
| `ollamaUrl` | `string` | `http://localhost:11434` | Base URL for the Ollama API used by the AI feedback panel |
52+
53+
## Requirements
54+
55+
- Node.js >= 22.18.0
56+
- React 19
57+
- TypeScript >= 5.8
58+
59+
## How It Works
60+
61+
Reactoscope is a set of Vite plugins that:
62+
63+
1. **Schema plugin** — Parses your component's TypeScript props at dev time and serves them as JSON, powering the auto-generated prop controls
64+
2. **Stress plugin** — Renders your component server-side in a loop, measuring timing, output determinism, and heap growth
65+
3. **AI plugin** — Proxies requests to a local Ollama instance, injecting your component's source code as context
66+
4. **UI plugin** — Serves the pre-built Reactoscope dashboard at `/__observatory`
67+
68+
The CLI wraps all of this into a single command using Vite's `createServer` API, so it works even in projects that don't use Vite.
69+
70+
## AI Feedback
71+
72+
The AI panel requires [Ollama](https://ollama.ai) running locally. Install it, pull a model, and the panel will auto-detect available models:
73+
74+
```bash
75+
ollama pull llama3
76+
```
77+
78+
## License
79+
80+
MIT

bin/observe.js

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,79 @@
11
#!/usr/bin/env node
22

3-
import { spawn } from 'node:child_process'
43
import { resolve, relative } from 'node:path'
54
import { fileURLToPath } from 'node:url'
5+
import { existsSync } from 'node:fs'
6+
import { createServer } from 'vite'
7+
import react from '@vitejs/plugin-react'
68

79
const componentPath = process.argv[2]
810

911
if (!componentPath) {
10-
console.error('Usage: observe path/to/MyComponent.tsx')
12+
console.error('Usage: reactoscope path/to/MyComponent.tsx')
1113
process.exit(1)
1214
}
1315

14-
const projectRoot = resolve(fileURLToPath(import.meta.url), '../..')
15-
const abs = resolve(componentPath)
16+
const cwd = process.cwd()
17+
const abs = resolve(cwd, componentPath)
1618

17-
// Make it relative to project root so we don't leak absolute paths
18-
const rel = relative(projectRoot, abs)
19+
if (!existsSync(abs)) {
20+
console.error(`Error: File not found: ${abs}`)
21+
process.exit(1)
22+
}
23+
24+
const rel = relative(cwd, abs)
1925

2026
if (rel.startsWith('..')) {
21-
console.error('Error: Component must be inside the project directory.')
27+
console.error(
28+
'Error: Component must be inside the current working directory.',
29+
)
2230
process.exit(1)
2331
}
2432

25-
const viteBin = resolve(projectRoot, 'node_modules/.bin/vite')
26-
27-
// spawn avoids shell injection — no shell is involved
28-
const existingNodeOptions = process.env.NODE_OPTIONS ?? ''
29-
const child = spawn(
30-
viteBin,
31-
['--open', `/?component=${encodeURIComponent(rel)}`],
32-
{
33-
cwd: projectRoot,
34-
stdio: 'inherit',
35-
env: {
36-
...process.env,
37-
NODE_OPTIONS: `${existingNodeOptions} --expose-gc`.trim(),
33+
const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
34+
35+
// Dynamic import so this works whether the user installed the package
36+
// or is running from the repo itself.
37+
const { observatory } = await import(resolve(pkgRoot, 'dist/plugin.mjs'))
38+
39+
/** Check whether the user's Vite config already includes a React plugin. */
40+
function hasReactPlugin(plugins) {
41+
const reactPluginNames = new Set([
42+
'vite:react-babel',
43+
'vite:react-swc',
44+
'vite:react-refresh',
45+
])
46+
return plugins.some((p) => {
47+
if (Array.isArray(p)) return p.some((pp) => reactPluginNames.has(pp.name))
48+
return reactPluginNames.has(p.name)
49+
})
50+
}
51+
52+
// Build the plugin list — always include observatory,
53+
// only add react() if the user's config doesn't already provide one.
54+
const extraPlugins = [...observatory()]
55+
56+
const server = await createServer({
57+
root: cwd,
58+
plugins: [
59+
{
60+
name: 'observatory:inject',
61+
config(config) {
62+
const existing = config.plugins?.flat() ?? []
63+
if (!hasReactPlugin(existing)) {
64+
config.plugins = [react(), ...existing]
65+
}
66+
},
3867
},
68+
...extraPlugins,
69+
],
70+
resolve: {
71+
tsconfigPaths: true,
72+
},
73+
server: {
74+
open: `/__observatory?component=${encodeURIComponent(rel)}`,
3975
},
40-
)
76+
})
4177

42-
child.on('exit', (code) => process.exit(code ?? 0))
78+
await server.listen()
79+
server.printUrls()

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>react-observatory</title>
7+
<title>Reactoscope</title>
88
</head>
99
<body>
1010
<div id="root"></div>

0 commit comments

Comments
 (0)