Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .cursor/skills/framework-doctor/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: framework-doctor
description: |
Framework Doctor scans Svelte and React projects for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.
Framework Doctor scans Svelte, React, and Vue projects for security, performance, correctness, and architecture issues. Outputs a 0-100 score with actionable diagnostics.

Use when: reviewing code, finishing a feature, fixing bugs, handling user input or HTML rendering, using eval/dynamic code, or when the user mentions XSS, dead code, svelte-check, knip, oxlint, framework-doctor, react-doctor, svelte-doctor.
Use when: reviewing code, finishing a feature, fixing bugs, handling user input or HTML rendering, using eval/dynamic code, or when the user mentions XSS, dead code, svelte-check, knip, oxlint, framework-doctor, react-doctor, svelte-doctor, vue-doctor.

Triggers on: security patterns, {@html}, eval(), new Function(), svelte-check, knip, oxlint, Svelte 5 migration, React best practices.
Triggers on: security patterns, {@html}, v-html, eval(), new Function(), svelte-check, knip, oxlint, Svelte 5 migration, React best practices, Vue/Nuxt patterns.
metadata:
version: 1.0.0
---
Expand All @@ -14,7 +14,7 @@ metadata:

Scans your frontend codebase for security, performance, correctness, and architecture issues. Auto-detects Svelte or React from `package.json`. Outputs a 0-100 score with actionable diagnostics.

**Supported:** Svelte, React (Vue, Angular coming soon)
**Supported:** Svelte, React, Vue (Angular coming soon)

## IMPORTANT: Run After Making Changes

Expand All @@ -29,6 +29,7 @@ Scan project?
├─ Auto-detect framework → npx -y @framework-doctor/cli . --verbose --diff
├─ Svelte only → npx -y @framework-doctor/svelte . --verbose --diff
├─ React only → npx -y @framework-doctor/react . --verbose --diff
├─ Vue only → npx -y @framework-doctor/vue . --verbose --diff
├─ Flags (verbose, diff, score) → references/cli/commands.md
└─ What gets checked → references/checks/RULE.md
```
Expand All @@ -44,6 +45,15 @@ Svelte guidance?
└─ What doctor checks → references/checks/RULE.md
```

### "I'm working with Vue"

```
Vue guidance?
├─ Run doctor → npx -y @framework-doctor/vue . --verbose --diff
├─ Security (v-html) → references/security/vue.md (if exists)
└─ What doctor checks → references/checks/RULE.md
```

### "I'm working with React"

```
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![npm version](https://img.shields.io/npm/v/@framework-doctor/cli.svg)](https://www.npmjs.com/package/@framework-doctor/cli)
[![npm downloads](https://img.shields.io/npm/dm/@framework-doctor/cli.svg)](https://www.npmjs.com/package/@framework-doctor/cli)

Framework Doctor auto-detects your framework and runs the right health check. Supports **Svelte** and **React**; Vue and Angular coming soon.
Framework Doctor auto-detects your framework and runs the right health check. Supports **Svelte**, **React**, and **Vue**; Angular coming soon.

## Quick start

Expand All @@ -18,6 +18,7 @@ Or run a specific doctor directly:
```bash
npx -y @framework-doctor/react . # React
npx -y @framework-doctor/svelte . # Svelte
npx -y @framework-doctor/vue . # Vue
```

## Try it
Expand Down Expand Up @@ -47,6 +48,14 @@ See [examples/README.md](examples/README.md) for more demo projects and commands
- `npx -y @framework-doctor/react . --verbose` - include file and line details
- `npx -y @framework-doctor/react . --score` - print only the numeric score (CI-friendly)

**Vue (direct):**

- `npx -y @framework-doctor/vue .` - run a full scan
- `npx -y @framework-doctor/vue . --verbose` - include file and line details
- `npx -y @framework-doctor/vue . --score` - print only the numeric score (CI-friendly)
- `npx -y @framework-doctor/vue . --diff main` - scan only files changed against `main`.
- `npx -y @framework-doctor/vue . --project web` - select a specific workspace package.

**Svelte (direct):**

- `npx -y @framework-doctor/svelte .` - run a full scan
Expand Down Expand Up @@ -79,7 +88,7 @@ Options:
-h, --help display help for command
```

React doctor options: `--no-lint`, `--no-dead-code`, `--verbose`, `--score`, `--no-analytics`, `--project`, `--diff`. See [packages/react-doctor/README.md](packages/react-doctor/README.md).
React doctor options: `--no-lint`, `--no-dead-code`, `--verbose`, `--score`, `--no-analytics`, `--project`, `--diff`, `--offline`. See [packages/react-doctor/README.md](packages/react-doctor/README.md).

## Security checks

Expand Down
28 changes: 28 additions & 0 deletions examples/vue/demo-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Framework Doctor Vue Demo

A minimal Vue 3 + Vite app with **intentional issues** for testing [Framework Doctor](https://github.com/pitis/framework-doctor).

## Run the doctor

From the framework-doctor repo root (after `pnpm install` and `pnpm build`):

```bash
pnpm exec framework-doctor examples/vue/demo-app
# or directly:
pnpm exec vue-doctor examples/vue/demo-app
```

## Intentional issues

- **Security** — `eval()`, `new Function()`, `setTimeout("string")` in `src/lib/SecurityTest.ts`
- **Dead code** — Unused exports in `src/lib/orphanUtils.ts`
- **v-html** — `v-html` with user content in `App.vue`
- **XSS** — `javascript:` URLs in `App.vue`
- **Accessibility** — div with `role="button"` (no keyboard support), empty anchor links

## Develop

```bash
pnpm install
pnpm dev
```
13 changes: 13 additions & 0 deletions examples/vue/demo-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Framework Doctor Vue Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/vue/demo-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "framework-doctor-vue-demo",
"version": "1.0.0",
"private": true,
"description": "Demo Vue app for Framework Doctor - includes intentional issues",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0",
"vue": "^3.0.0",
"vue-tsc": "^2.1.10"
},
"packageManager": "pnpm@10.30.0"
}
1 change: 1 addition & 0 deletions examples/vue/demo-app/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions examples/vue/demo-app/src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { ref } from 'vue';
import DoctorTestComponent from './components/DoctorTestComponent.vue';

const darkMode = ref(false);
const unusedState = ref(42);
const userRenderedContent = ref('<img src="invalid:" onerror="alert(1)">');
const dynamicJsUrl = ref('javascript:void(0)');

const toggleDarkMode = () => {
darkMode.value = !darkMode.value;
};
</script>

<template>
<div class="container">
<h1>Framework Doctor Vue Demo</h1>
<p>A minimal Vue app with intentional issues for vue-doctor testing.</p>

<div class="example">
<button type="button" @click="toggleDarkMode">
{{ darkMode ? '☀️ Light' : '🌙 Dark' }}
</button>
</div>

<hr />
<h2>vue-doctor test section (intentional issues)</h2>
<div class="example">
<span v-for="item in ['a', 'b', 'c']" :key="item">{{ item }}</span>

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" />

<div role="button" @click="darkMode = !darkMode">Toggle (div, no keyboard)</div>

<a href="#"> </a>

<p>Unused state value: {{ unusedState }}</p>

<DoctorTestComponent label="Test" />

<div v-html="userRenderedContent" />

<a href="javascript:alert('XSS')">Click me (javascript: URL)</a>
<a :href="dynamicJsUrl">Dynamic JS URL</a>
</div>
</div>
</template>

<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}

hr {
margin: 2rem 0;
border: none;
border-top: 1px solid #e0e0e0;
}
</style>
11 changes: 11 additions & 0 deletions examples/vue/demo-app/src/components/DoctorTestComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
defineProps<{ label: string }>();

const handleClick = () => {
console.log('clicked');
};
</script>

<template>
<button type="button" @click="handleClick">{{ label }}</button>
</template>
13 changes: 13 additions & 0 deletions examples/vue/demo-app/src/lib/SecurityTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* INTENTIONAL SECURITY ISSUES for vue-doctor testing.
* These are dangerous patterns that should be flagged by linters.
*/

export const dangerousEval = (userInput: string): unknown => eval(userInput);

export const dangerousFunction = (userCode: string): (() => void) =>
new Function(userCode) as () => void;

export const dangerousTimeout = (): void => {
setTimeout("console.log('arbitrary code')", 100);
};
8 changes: 8 additions & 0 deletions examples/vue/demo-app/src/lib/orphanUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* INTENTIONAL: Unused file for vue-doctor testing.
* This file is not imported anywhere - knip will report it as unused.
*/

export const ORPHAN_CONSTANT = 42;

export const orphanHelper = (value: number): number => value * 2;
5 changes: 5 additions & 0 deletions examples/vue/demo-app/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
import './style.css';

createApp(App).mount('#app');
13 changes: 13 additions & 0 deletions examples/vue/demo-app/src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#app {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}

.example {
margin: 1rem 0;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
7 changes: 7 additions & 0 deletions examples/vue/demo-app/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />

declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<object, object, unknown>;
export default component;
}
21 changes: 21 additions & 0 deletions examples/vue/demo-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
9 changes: 9 additions & 0 deletions examples/vue/demo-app/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}
2 changes: 2 additions & 0 deletions examples/vue/demo-app/vite.config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;
5 changes: 5 additions & 0 deletions examples/vue/demo-app/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});
6 changes: 6 additions & 0 deletions examples/vue/demo-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
plugins: [vue()],
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"build": "turbo run build --filter=./packages/*",
"build:svelte": "turbo run build --filter=@framework-doctor/svelte",
"build:cli": "turbo run build --filter=@framework-doctor/cli",
"demo": "pnpm build && pnpm exec framework-doctor examples/svelte/demo-app",
"demo:svelte": "pnpm build && pnpm exec framework-doctor examples/svelte/demo-app",
"demo:vue": "pnpm build && pnpm exec framework-doctor examples/vue/demo-app",
"dev": "turbo run dev",
"dev:doctor": "turbo run dev --filter=@framework-doctor/svelte",
"format": "prettier --write .",
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# @framework-doctor/cli

## 1.0.3

### Patch Changes

- vue doctor
- Updated dependencies
- @framework-doctor/svelte@1.0.3
- @framework-doctor/react@1.0.3
- @framework-doctor/vue@1.0.3

## Unreleased

### Minor Changes

- Add Vue support: auto-detect vue/nuxt and run @framework-doctor/vue
- Remove Vue from "coming soon"; Angular remains coming soon

## 1.0.2

### Patch Changes
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@framework-doctor/cli",
"version": "1.0.2",
"version": "1.0.3",
"description": "Auto-detect framework and run the right doctor",
"author": {
"name": "Pitis Radu",
Expand Down Expand Up @@ -46,7 +46,8 @@
},
"dependencies": {
"@framework-doctor/svelte": "workspace:*",
"@framework-doctor/react": "workspace:*"
"@framework-doctor/react": "workspace:*",
"@framework-doctor/vue": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
Loading