├── .gitignore ├── playground ├── app │ ├── assets │ │ ├── libeccio.webp │ │ └── maestrale.webp │ ├── layouts │ │ └── center.vue │ ├── middleware │ │ └── auth.ts │ ├── pages │ │ ├── about.vue │ │ └── about │ │ │ └── contact.vue │ ├── utils │ │ └── index.ts │ ├── components │ │ └── foo-bar.vue │ └── app.vue ├── public │ └── fallback.json ├── server │ ├── api │ │ ├── foo.get.ts │ │ └── foo.post.ts │ └── routes │ │ ├── sitemap.get.ts │ │ └── sitemap.post.ts ├── .vscode │ └── settings.json ├── layers │ ├── archer │ │ └── nuxt.config.ts │ └── berserker │ │ └── nuxt.config.ts ├── .gitignore ├── tsconfig.json ├── nuxt.config.ts └── package.json ├── eslint.config.js ├── tsconfig.json ├── .editorconfig ├── packages ├── unimport │ ├── tsdown.config.ts │ ├── package.json │ ├── README.md │ └── src │ │ └── index.ts ├── shared │ ├── package.json │ └── src │ │ └── index.ts └── nuxt │ ├── src │ ├── event │ │ ├── types.ts │ │ ├── server.ts │ │ └── client.ts │ ├── typescript │ │ ├── features │ │ │ ├── findRenameLocations.ts │ │ │ ├── findReferences.ts │ │ │ ├── getEditsForFileRename.ts │ │ │ └── getDefinitionAndBoundSpan.ts │ │ ├── types.ts │ │ ├── data.ts │ │ ├── utils.ts │ │ └── index.ts │ └── module │ │ ├── events.ts │ │ └── index.ts │ ├── tsdown.config.ts │ ├── package.json │ └── README.md ├── scripts ├── publish.ts └── release.ts ├── README.md ├── package.json ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── pnpm-workspace.yaml ├── .vscode └── launch.json ├── LICENSE └── test ├── __snapshots__ └── playground.test.ts.snap └── playground.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /playground/app/assets/libeccio.webp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/app/assets/maestrale.webp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/public/fallback.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /playground/server/api/foo.get.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => {}); 2 | -------------------------------------------------------------------------------- /playground/server/api/foo.post.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => {}); 2 | -------------------------------------------------------------------------------- /playground/app/layouts/center.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/server/routes/sitemap.get.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => {}); 2 | -------------------------------------------------------------------------------- /playground/app/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(() => {}); 2 | -------------------------------------------------------------------------------- /playground/app/pages/about.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/server/routes/sitemap.post.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => {}); 2 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import zin from "@zinkawaii/eslint-config"; 2 | 3 | export default zin(); 4 | -------------------------------------------------------------------------------- /playground/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@zinkawaii/tsconfig", 3 | "include": [ 4 | "packages/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /playground/app/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* -------------- auto imports -------------- */ 2 | 3 | export const foo = 1; 4 | // ^—^(references) 5 | -------------------------------------------------------------------------------- /playground/layers/archer/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | runtimeConfig: { 3 | foo: { 4 | bar: 1, 5 | }, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /playground/layers/berserker/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | runtimeConfig: { 3 | foo: { 4 | bar: 1, 5 | baz: 2, 6 | }, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /playground/app/pages/about/contact.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{md,json,yml,yaml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /packages/unimport/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig({ 4 | format: [ 5 | "cjs", 6 | ], 7 | exports: true, 8 | noExternal: [ 9 | "@dxup/shared", 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dxup/shared", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "exports": { 7 | ".": "./src/index.ts" 8 | }, 9 | "devDependencies": { 10 | "typescript": "catalog:" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /playground/app/components/foo-bar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | .... 13 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "pathe"; 2 | import { $ } from "zx"; 3 | 4 | const tag = process.env.GITHUB_REF_NAME!; 5 | const packageName = tag.split("@")[1].slice("dxup/".length); 6 | 7 | await $({ 8 | cwd: resolve(import.meta.dirname, "../packages", packageName), 9 | })`pnpm publish --access public --no-git-checks`; 10 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./.nuxt/tsconfig.app.json" 5 | }, 6 | { 7 | "path": "./.nuxt/tsconfig.server.json" 8 | }, 9 | { 10 | "path": "./.nuxt/tsconfig.shared.json" 11 | }, 12 | { 13 | "path": "./.nuxt/tsconfig.node.json" 14 | } 15 | ], 16 | "files": [] 17 | } 18 | -------------------------------------------------------------------------------- /packages/nuxt/src/event/types.ts: -------------------------------------------------------------------------------- 1 | import type ts from "typescript"; 2 | 3 | export interface ComponentReferenceInfo { 4 | textSpan: ts.TextSpan; 5 | lazy?: boolean; 6 | } 7 | 8 | export interface EventMap { 9 | "components:rename": [data: { 10 | fileName: string; 11 | references: Record; 12 | }]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/nuxt/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig([{ 4 | entry: { 5 | module: "src/module/index.ts", 6 | }, 7 | }, { 8 | entry: { 9 | typescript: "src/typescript/index.ts", 10 | }, 11 | format: [ 12 | "cjs", 13 | ], 14 | noExternal: [ 15 | "@dxup/shared", 16 | ], 17 | }]); 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dxup 2 | 3 | Dxup is a collection of plugins to improve your development experience. 4 | 5 | ## Packages 6 | 7 | - [@dxup/nuxt](/packages/nuxt) 8 | - [@dxup/unimport](/packages/unimport) 9 | 10 | ## Notes 11 | 12 | If you are using VS Code, to allow TS server to load the local plugins, please: 13 | 14 | > Run the "TypeScript: Select TypeScript Version" command and choose "Use Workspace Version". 15 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import { versionBump } from "bumpp"; 3 | import { join } from "pathe"; 4 | 5 | const path = join(process.cwd(), "package.json"); 6 | const text = await readFile(path, "utf-8"); 7 | const { name } = JSON.parse(text); 8 | 9 | await versionBump({ 10 | push: false, 11 | tag: `${name}@%s`, 12 | commit: `release(${name.slice("@dxup/".length)}): v%s`, 13 | }); 14 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | const runtimeConfig = { 2 | foo: { 3 | bar: 1, 4 | baz: 2, 5 | qux: 3, 6 | }, 7 | public: { 8 | hello: 2333, 9 | }, 10 | }; 11 | 12 | export default defineNuxtConfig({ 13 | compatibilityDate: "2025-07-15", 14 | experimental: { 15 | typedPages: true, 16 | }, 17 | runtimeConfig, 18 | modules: [ 19 | "@dxup/nuxt", 20 | ], 21 | }); 22 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt dev" 7 | }, 8 | "dependencies": { 9 | "@dxup/nuxt": "workspace:", 10 | "nuxt": "catalog:", 11 | "vue": "catalog:", 12 | "vue-router": "catalog:" 13 | }, 14 | "devDependencies": { 15 | "@vue/typescript-plugin": "catalog:", 16 | "typescript": "catalog:" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "packageManager": "pnpm@10.25.0", 5 | "scripts": { 6 | "build": "pnpm --filter @dxup/* build", 7 | "dev": "pnpm --filter @dxup/* --parallel dev", 8 | "publish": "node scripts/publish.ts", 9 | "test": "vitest" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "catalog:", 13 | "@zinkawaii/eslint-config": "catalog:", 14 | "@zinkawaii/tsconfig": "catalog:", 15 | "bumpp": "catalog:", 16 | "eslint": "catalog:", 17 | "pathe": "catalog:", 18 | "tsdown": "catalog:", 19 | "vitest": "catalog:", 20 | "zx": "catalog:" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "**" 8 | 9 | permissions: 10 | id-token: write 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | 18 | - uses: pnpm/action-setup@v4 19 | 20 | - uses: actions/setup-node@v5 21 | with: 22 | node-version: 24 23 | cache: pnpm 24 | 25 | - name: Install Dependencies 26 | run: pnpm install 27 | 28 | - name: Build 29 | run: pnpm run build 30 | 31 | - name: Publish 32 | run: pnpm run publish 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | 13 | - uses: pnpm/action-setup@v4 14 | 15 | - uses: actions/setup-node@v5 16 | with: 17 | node-version: 24 18 | cache: pnpm 19 | 20 | - name: Install Dependencies 21 | run: pnpm install 22 | 23 | - name: Build 24 | run: pnpm run build 25 | 26 | - name: Prepare Playground 27 | run: pnpm nuxt prepare playground 28 | 29 | - name: Run Tests 30 | run: pnpm run test 31 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/features/findRenameLocations.ts: -------------------------------------------------------------------------------- 1 | import type ts from "typescript"; 2 | import type { Context } from "../types"; 3 | 4 | export function preprocess( 5 | context: Context, 6 | findRenameLocations: ts.LanguageService["findRenameLocations"], 7 | ): ts.LanguageService["findRenameLocations"] { 8 | const { data } = context; 9 | 10 | return (...args) => { 11 | // @ts-expect-error union args cannot satisfy deprecated overload 12 | const result = findRenameLocations(...args); 13 | 14 | return result?.filter((edit) => { 15 | return !edit.fileName.startsWith(data.buildDir); 16 | }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/nuxt/src/event/server.ts: -------------------------------------------------------------------------------- 1 | import { appendFile } from "node:fs/promises"; 2 | import { join } from "pathe"; 3 | import type ts from "typescript"; 4 | import type { EventMap } from "./types"; 5 | 6 | export function createEventServer(info: ts.server.PluginCreateInfo) { 7 | const path = join(info.project.getCurrentDirectory(), "dxup/events.md"); 8 | 9 | async function write(key: K, data: EventMap[K][0]) { 10 | try { 11 | await appendFile(path, `\`\`\`json {${key}}\n${JSON.stringify(data, null, 2)}\n\`\`\`\n`); 12 | } 13 | // TODO: 14 | catch {} 15 | } 16 | 17 | return { 18 | write, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | shamefullyHoist: true 2 | 3 | packages: 4 | - packages/* 5 | - playground 6 | 7 | catalog: 8 | "@nuxt/kit": ^4.2.2 9 | "@types/node": ^24.10.2 10 | "@volar/language-core": ^2.4.26 11 | "@volar/typescript": ^2.4.26 12 | "@vue/language-core": ^3.1.8 13 | "@vue/typescript-plugin": ^3.1.8 14 | "@zinkawaii/eslint-config": ^0.4.1 15 | "@zinkawaii/tsconfig": ^0.0.2 16 | bumpp: ^10.3.2 17 | chokidar: ^5.0.0 18 | eslint: ^9.39.1 19 | nuxt: ^4.2.2 20 | pathe: ^2.0.3 21 | tinyglobby: ^0.2.15 22 | tsdown: ^0.17.2 23 | typescript: ^5.9.3 24 | vitest: ^4.0.15 25 | vue: ^3.5.25 26 | vue-router: ^4.6.3 27 | zx: ^8.8.5 28 | 29 | onlyBuiltDependencies: 30 | - "@parcel/watcher" 31 | - esbuild 32 | -------------------------------------------------------------------------------- /packages/unimport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dxup/unimport", 3 | "type": "module", 4 | "version": "0.1.2", 5 | "description": "TypeScript plugin for unimport", 6 | "author": "KazariEX", 7 | "license": "MIT", 8 | "repository": "KazariEX/dxup", 9 | "exports": { 10 | ".": "./dist/index.cjs", 11 | "./package.json": "./package.json" 12 | }, 13 | "main": "./dist/index.cjs", 14 | "types": "./dist/index.d.cts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsdown", 20 | "dev": "tsdown -w --sourcemap", 21 | "prepack": "pnpm run build", 22 | "release": "node ../../scripts/release.ts" 23 | }, 24 | "devDependencies": { 25 | "@dxup/shared": "workspace:", 26 | "typescript": "catalog:" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "compounds": [ 4 | { 5 | "name": "Debug", 6 | "configurations": [ 7 | "Launch Playground", 8 | "Attach to TS Server" 9 | ], 10 | "presentation": { 11 | "order": 0 12 | } 13 | } 14 | ], 15 | "configurations": [ 16 | { 17 | "name": "Launch Playground", 18 | "type": "node", 19 | "request": "launch", 20 | "runtimeExecutable": "code-insiders", 21 | "args": [ 22 | "playground" 23 | ], 24 | "env": { 25 | "TSS_DEBUG": "5859" 26 | } 27 | }, 28 | { 29 | "name": "Attach to TS Server", 30 | "type": "node", 31 | "request": "attach", 32 | "port": 5859, 33 | "restart": true, 34 | "outFiles": [ 35 | "${workspaceRoot}/packages/*/dist/**/*.{c|m|}js" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /packages/unimport/README.md: -------------------------------------------------------------------------------- 1 | # @dxup/unimport 2 | 3 | [![version](https://img.shields.io/npm/v/@dxup/unimport?color=007EC7&label=npm)](https://www.npmjs.com/package/@dxup/unimport) 4 | [![downloads](https://img.shields.io/npm/dm/@dxup/unimport?color=007EC7&label=downloads)](https://www.npmjs.com/package/@dxup/unimport) 5 | [![license](https://img.shields.io/npm/l/@dxup/unimport?color=007EC7&label=license)](/LICENSE) 6 | 7 | This is a TypeScript plugin that reduces user friction when using navigation features on auto imported variables generated by `unimport`. It aims to make the generated declaration files transparent to users. 8 | 9 | ## Features 10 | 11 | - [x] Go to Definition 12 | - [x] Go to References 13 | - [x] Rename Symbol 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pnpm i -D @dxup/unimport 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add the following to your `tsconfig.json`: 24 | 25 | ```json 26 | { 27 | "compilerOptions": { 28 | "plugins": [ 29 | { "name": "@dxup/unimport" } 30 | ] 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dxup/nuxt", 3 | "type": "module", 4 | "version": "0.3.2", 5 | "description": "TypeScript plugin for Nuxt", 6 | "author": "KazariEX", 7 | "license": "MIT", 8 | "repository": "KazariEX/dxup", 9 | "exports": { 10 | ".": "./dist/module.mjs", 11 | "./package.json": "./package.json" 12 | }, 13 | "main": "./dist/typescript.cjs", 14 | "types": "./dist/typescript.d.cts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsdown", 20 | "dev": "tsdown -w --sourcemap", 21 | "prepack": "pnpm run build", 22 | "release": "node ../../scripts/release.ts" 23 | }, 24 | "dependencies": { 25 | "@dxup/unimport": "workspace:^", 26 | "@nuxt/kit": "catalog:", 27 | "chokidar": "catalog:", 28 | "pathe": "catalog:", 29 | "tinyglobby": "catalog:" 30 | }, 31 | "devDependencies": { 32 | "@dxup/shared": "workspace:", 33 | "@volar/language-core": "catalog:", 34 | "@volar/typescript": "catalog:", 35 | "@vue/language-core": "catalog:", 36 | "nuxt": "catalog:", 37 | "typescript": "catalog:" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025-present KazariEX 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. 22 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/types.ts: -------------------------------------------------------------------------------- 1 | import type { Language } from "@volar/language-core"; 2 | import type ts from "typescript"; 3 | import type { createEventServer } from "../event/server"; 4 | 5 | export interface Data { 6 | buildDir: string; 7 | publicDir: string; 8 | configFiles: string[]; 9 | layouts: { 10 | [name: string]: string; 11 | }; 12 | middleware: { 13 | [name: string]: string; 14 | }; 15 | nitroRoutes: { 16 | [route: string]: { 17 | [method: string]: string; 18 | }; 19 | }; 20 | typedPages: { 21 | [name: string]: string; 22 | }; 23 | features: { 24 | components: boolean; 25 | importGlob: boolean; 26 | nitroRoutes: boolean; 27 | pageMeta: boolean; 28 | runtimeConfig: boolean; 29 | typedPages: boolean; 30 | unimport: { 31 | componentReferences: boolean; 32 | }; 33 | }; 34 | } 35 | 36 | export interface Context { 37 | ts: typeof import("typescript"); 38 | info: ts.server.PluginCreateInfo; 39 | data: Data; 40 | server: ReturnType; 41 | language?: Language; 42 | } 43 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | import type ts from "typescript"; 2 | 3 | export function* forEachTouchingNode( 4 | ts: typeof import("typescript"), 5 | sourceFile: ts.SourceFile, 6 | position: number, 7 | ) { 8 | yield* binaryVisit(ts, sourceFile, sourceFile, position); 9 | } 10 | 11 | function* binaryVisit( 12 | ts: typeof import("typescript"), 13 | sourceFile: ts.SourceFile, 14 | node: ts.Node, 15 | position: number, 16 | ): Generator { 17 | const nodes: ts.Node[] = []; 18 | ts.forEachChild(node, (child) => { 19 | nodes.push(child); 20 | }); 21 | 22 | let left = 0; 23 | let right = nodes.length - 1; 24 | 25 | while (left <= right) { 26 | const mid = Math.floor((left + right) / 2); 27 | const node = nodes[mid]; 28 | 29 | if (position > node.getEnd()) { 30 | left = mid + 1; 31 | } 32 | else if (position < node.getStart(sourceFile)) { 33 | right = mid - 1; 34 | } 35 | else { 36 | yield node; 37 | yield* binaryVisit(ts, sourceFile, node, position); 38 | return; 39 | } 40 | } 41 | } 42 | 43 | export function isTextSpanWithin( 44 | node: ts.Node, 45 | textSpan: ts.TextSpan, 46 | sourceFile: ts.SourceFile, 47 | ) { 48 | return ( 49 | textSpan.start + textSpan.length <= node.getEnd() && 50 | textSpan.start >= node.getStart(sourceFile) 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/nuxt/src/module/events.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | import type { Nuxt } from "nuxt/schema"; 3 | import type { EventMap } from "../event/types"; 4 | 5 | const uppercaseRE = /[A-Z]/; 6 | 7 | export async function onComponentsRename( 8 | nuxt: Nuxt, 9 | { fileName, references }: EventMap["components:rename"][0], 10 | ) { 11 | const component = Object.values(nuxt.apps) 12 | .flatMap((app) => app.components) 13 | .find((c) => c.filePath === fileName); 14 | if (!component) { 15 | return; 16 | } 17 | 18 | const tasks = Object.entries(references).map(async ([fileName, references]) => { 19 | const code = await readFile(fileName, "utf-8"); 20 | const chunks: string[] = []; 21 | let offset = 0; 22 | for (const { textSpan, lazy } of references) { 23 | const start = textSpan.start; 24 | const end = start + textSpan.length; 25 | const oldName = code.slice(start, end); 26 | const newName = uppercaseRE.test(oldName) 27 | ? lazy ? "Lazy" + component.pascalName : component.pascalName 28 | : lazy ? "lazy-" + component.kebabName : component.kebabName; 29 | chunks.push(code.slice(offset, start), newName); 30 | offset = end; 31 | } 32 | chunks.push(code.slice(offset)); 33 | await writeFile(fileName, chunks.join("")); 34 | }); 35 | 36 | await Promise.all(tasks); 37 | } 38 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/data.ts: -------------------------------------------------------------------------------- 1 | import { join } from "pathe"; 2 | import type ts from "typescript"; 3 | import type { Data } from "./types"; 4 | 5 | const initialValue: Data = { 6 | buildDir: "", 7 | publicDir: "", 8 | configFiles: [], 9 | layouts: {}, 10 | middleware: {}, 11 | nitroRoutes: {}, 12 | typedPages: {}, 13 | features: { 14 | components: true, 15 | importGlob: true, 16 | nitroRoutes: true, 17 | pageMeta: true, 18 | runtimeConfig: true, 19 | typedPages: true, 20 | unimport: { 21 | componentReferences: true, 22 | }, 23 | }, 24 | }; 25 | 26 | const callbacks: Record void)[]> = {}; 27 | 28 | export function createData(ts: typeof import("typescript"), info: ts.server.PluginCreateInfo) { 29 | const currentDirectory = info.languageServiceHost.getCurrentDirectory(); 30 | const path = join(currentDirectory, "dxup/data.json"); 31 | const data = {} as Data; 32 | 33 | const updates = callbacks[path] ??= ( 34 | ts.sys.watchFile?.(path, () => { 35 | const text = ts.sys.readFile(path); 36 | for (const update of updates) { 37 | update(text); 38 | } 39 | }), [] 40 | ); 41 | 42 | updates.push((text) => { 43 | Object.assign(data, { 44 | ...initialValue, 45 | ...text ? JSON.parse(text) : {}, 46 | }); 47 | }); 48 | 49 | const text = ts.sys.readFile(path); 50 | updates.at(-1)!(text); 51 | 52 | return data; 53 | } 54 | -------------------------------------------------------------------------------- /packages/nuxt/src/event/client.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "node:buffer"; 2 | import EventEmitter from "node:events"; 3 | import { mkdir, open, writeFile } from "node:fs/promises"; 4 | import { watch } from "chokidar"; 5 | import { dirname, join } from "pathe"; 6 | import type { Nuxt } from "nuxt/schema"; 7 | import type { EventMap } from "./types"; 8 | 9 | const responseRE = /^```json \{(?.*)\}\n(?[\s\S]*?)\n```$/; 10 | 11 | export async function createEventClient(nuxt: Nuxt) { 12 | const path = join(nuxt.options.buildDir, "dxup/events.md"); 13 | await mkdir(dirname(path), { recursive: true }); 14 | await writeFile(path, ""); 15 | 16 | const fd = await open(path, "r"); 17 | const watcher = watch(path, { 18 | ignoreInitial: true, 19 | }); 20 | 21 | nuxt.hook("close", async () => { 22 | await fd.close(); 23 | await watcher.close(); 24 | }); 25 | 26 | const client = new EventEmitter(); 27 | let offset = 0; 28 | 29 | watcher.on("change", async (path, stats) => { 30 | if (!stats || stats.size <= offset) { 31 | return; 32 | } 33 | const pos = offset; 34 | offset = stats.size; 35 | 36 | const buffer = Buffer.alloc(offset - pos); 37 | await fd.read(buffer, 0, buffer.length, pos); 38 | const text = buffer.toString("utf-8").trim(); 39 | 40 | const match = text.match(responseRE); 41 | if (match) { 42 | const { key, value } = match.groups!; 43 | // @ts-expect-error [any] cannot satisfy never 44 | client.emit(key, JSON.parse(value)); 45 | } 46 | }); 47 | 48 | return client; 49 | } 50 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/features/findReferences.ts: -------------------------------------------------------------------------------- 1 | import type { Language } from "@volar/language-core"; 2 | import type ts from "typescript"; 3 | import { isVueVirtualCode, withVirtualOffset } from "../utils"; 4 | import type { Context } from "../types"; 5 | 6 | export function postprocess( 7 | context: Context, 8 | language: Language, 9 | findReferences: ts.LanguageService["findReferences"], 10 | ): ts.LanguageService["findReferences"] { 11 | const { ts, info } = context; 12 | 13 | return (...args) => { 14 | const result = findReferences(...args); 15 | 16 | if (!result?.length) { 17 | const sourceScript = language.scripts.get(args[0]); 18 | const root = sourceScript?.generated?.root; 19 | if (!isVueVirtualCode(root)) { 20 | return; 21 | } 22 | 23 | const start = (root.sfc.template?.start ?? Infinity) + 1; 24 | if (args[1] < start || args[1] > start + "template".length) { 25 | return; 26 | } 27 | 28 | const program = info.languageService.getProgram()!; 29 | const sourceFile = program.getSourceFile(args[0]); 30 | if (!sourceFile) { 31 | return; 32 | } 33 | 34 | for (const statement of sourceFile.statements) { 35 | if (ts.isExportAssignment(statement)) { 36 | const defaultKeyword = statement.getChildAt(1); 37 | return withVirtualOffset( 38 | language, 39 | sourceScript!, 40 | defaultKeyword.getStart(sourceFile), 41 | (position) => findReferences(args[0], position), 42 | ); 43 | } 44 | } 45 | return; 46 | } 47 | 48 | return result; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /playground/app/app.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | 65 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CodeMapping, Language, SourceScript, VirtualCode } from "@volar/language-core"; 2 | import type { VueVirtualCode } from "@vue/language-core"; 3 | import type ts from "typescript"; 4 | 5 | export function createModuleDefinition(ts: typeof import("typescript"), path: string): ts.DefinitionInfo { 6 | return { 7 | fileName: path, 8 | textSpan: { start: 0, length: 0 }, 9 | kind: ts.ScriptElementKind.moduleElement, 10 | name: `"${path}"`, 11 | containerKind: ts.ScriptElementKind.unknown, 12 | containerName: "", 13 | }; 14 | } 15 | 16 | export function isVueVirtualCode(code?: VirtualCode): code is VueVirtualCode { 17 | return code?.languageId === "vue"; 18 | } 19 | 20 | export function withVirtualOffset( 21 | language: Language, 22 | sourceScript: SourceScript, 23 | position: number, 24 | method: (position: number) => R, 25 | ) { 26 | const serviceScript = sourceScript.generated!.languagePlugin.typescript?.getServiceScript( 27 | sourceScript.generated!.root, 28 | ); 29 | if (!serviceScript) { 30 | return; 31 | } 32 | 33 | const map = language.maps.get(serviceScript.code, sourceScript); 34 | const leadingOffset = sourceScript.snapshot.getLength(); 35 | 36 | const offset = 1145141919810; 37 | const mapping: CodeMapping = { 38 | sourceOffsets: [offset], 39 | generatedOffsets: [position - leadingOffset], 40 | lengths: [0], 41 | data: { 42 | completion: true, 43 | navigation: true, 44 | semantic: true, 45 | verification: true, 46 | }, 47 | }; 48 | 49 | const original = map.toGeneratedLocation; 50 | map.toGeneratedLocation = function *(sourceOffset, ...args) { 51 | if (sourceOffset === offset) { 52 | yield [mapping.generatedOffsets[0], mapping]; 53 | } 54 | yield* original.call(this, sourceOffset, ...args); 55 | }; 56 | 57 | try { 58 | return method(offset); 59 | } 60 | finally { 61 | map.toGeneratedLocation = original; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/features/getEditsForFileRename.ts: -------------------------------------------------------------------------------- 1 | import { forEachTouchingNode } from "@dxup/shared"; 2 | import type ts from "typescript"; 3 | import type { ComponentReferenceInfo } from "../../event/types"; 4 | import type { Context } from "../types"; 5 | 6 | export function preprocess( 7 | context: Context, 8 | getEditsForFileRename: ts.LanguageService["getEditsForFileRename"], 9 | ): ts.LanguageService["getEditsForFileRename"] { 10 | const { ts, info, data, server } = context; 11 | 12 | return (...args) => { 13 | const result = getEditsForFileRename(...args); 14 | if (!result?.length) { 15 | return result; 16 | } 17 | 18 | // use the language service proxied by volar for source offsets 19 | const languageService = info.project.getLanguageService(); 20 | const program = languageService.getProgram()!; 21 | const references: Record = {}; 22 | 23 | for (const change of result) { 24 | const { fileName, textChanges } = change; 25 | 26 | if (data.features.components && fileName.endsWith("components.d.ts")) { 27 | const sourceFile = program.getSourceFile(fileName); 28 | if (!sourceFile) { 29 | continue; 30 | } 31 | 32 | for (const { span } of textChanges) { 33 | for (const node of forEachTouchingNode(ts, sourceFile, span.start)) { 34 | if (!ts.isPropertySignature(node) && !ts.isVariableDeclaration(node)) { 35 | continue; 36 | } 37 | 38 | const position = node.name.getStart(sourceFile); 39 | const res = languageService.getReferencesAtPosition(fileName, position) 40 | ?.filter((entry) => !entry.fileName.startsWith(data.buildDir)) 41 | ?.sort((a, b) => a.textSpan.start - b.textSpan.start); 42 | 43 | const lazy = node.type && 44 | ts.isTypeReferenceNode(node.type) && 45 | ts.isIdentifier(node.type.typeName) && 46 | node.type.typeName.text === "LazyComponent"; 47 | 48 | for (const { fileName, textSpan } of res ?? []) { 49 | (references[fileName] ??= []).push({ 50 | textSpan, 51 | lazy: lazy || void 0, 52 | }); 53 | } 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | if (Object.keys(references).length) { 61 | server.write("components:rename", { 62 | fileName: args[1], 63 | references, 64 | }); 65 | } 66 | 67 | return result.filter((change) => { 68 | return !change.fileName.startsWith(data.buildDir); 69 | }); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /packages/nuxt/src/typescript/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type ts from "typescript"; 4 | import { createEventServer } from "../event/server"; 5 | import { createData } from "./data"; 6 | import * as findReferences from "./features/findReferences"; 7 | import * as findRenameLocations from "./features/findRenameLocations"; 8 | import * as getDefinitionAndBoundSpan from "./features/getDefinitionAndBoundSpan"; 9 | import * as getEditsForFileRename from "./features/getEditsForFileRename"; 10 | import type { Context } from "./types"; 11 | 12 | const plugin: ts.server.PluginModuleFactory = (module) => { 13 | const { typescript: ts } = module; 14 | 15 | return { 16 | create(info) { 17 | const data = createData(ts, info); 18 | const server = createEventServer(info); 19 | 20 | const context: Context = { ts, info, data, server }; 21 | queueMicrotask(() => { 22 | context.language = (info.project as any).__vue__?.language; 23 | 24 | if (!context.language || !data.features.unimport.componentReferences) { 25 | return; 26 | } 27 | 28 | // Because the volar based plugin is loaded latest, 29 | // it prevents the current plugin from accessing the original position 30 | // at the time the language service request is triggered. 31 | // If no mapping exists for that position, the request will be simply skipped. 32 | const languageService = info.project.getLanguageService(); 33 | const methods: Record = {}; 34 | 35 | for (const [key, method] of [ 36 | ["findReferences", findReferences], 37 | ["getDefinitionAndBoundSpan", getDefinitionAndBoundSpan], 38 | ] as const) { 39 | const original = languageService[key]; 40 | methods[key] = method.postprocess(context, context.language, original as any); 41 | } 42 | 43 | // eslint-disable-next-line dot-notation 44 | info.project["languageService"] = new Proxy(languageService, { 45 | get(target, p, receiver) { 46 | return methods[p] ?? Reflect.get(target, p, receiver); 47 | }, 48 | set(...args) { 49 | return Reflect.set(...args); 50 | }, 51 | }); 52 | }); 53 | 54 | for (const [key, method] of [ 55 | ["findRenameLocations", findRenameLocations], 56 | ["getDefinitionAndBoundSpan", getDefinitionAndBoundSpan], 57 | ["getEditsForFileRename", getEditsForFileRename], 58 | ] as const) { 59 | const original = info.languageService[key]; 60 | info.languageService[key] = method.preprocess(context, original as any) as any; 61 | } 62 | 63 | return info.languageService; 64 | }, 65 | }; 66 | }; 67 | 68 | export default plugin; 69 | -------------------------------------------------------------------------------- /packages/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # @dxup/nuxt 2 | 3 | [![version](https://img.shields.io/npm/v/@dxup/nuxt?color=007EC7&label=npm)](https://www.npmjs.com/package/@dxup/nuxt) 4 | [![downloads](https://img.shields.io/npm/dm/@dxup/nuxt?color=007EC7&label=downloads)](https://www.npmjs.com/package/@dxup/nuxt) 5 | [![license](https://img.shields.io/npm/l/@dxup/nuxt?color=007EC7&label=license)](/LICENSE) 6 | 7 | This is a TypeScript plugin that improves Nuxt DX. 8 | 9 | > [!note] 10 | > It's now an experimental builtin feature of Nuxt. Please refer to the [documentation](https://nuxt.com/docs/4.x/guide/going-further/experimental-features#typescriptplugin) for more details. 11 | 12 | ## Installation 13 | 14 | *No installation is required if you are using Nuxt 4.2 or above.* 15 | 16 | ## Usage 17 | 18 | 1. Have `@dxup/unimport` installed as a dependency if you haven't enabled the `shamefullyHoist` option with pnpm workspace. 19 | 20 | 2. Add the following to your `nuxt.config.ts`: 21 | 22 | ```ts 23 | export default defineNuxtConfig({ 24 | experimental: { 25 | typescriptPlugin: true, 26 | }, 27 | }); 28 | ``` 29 | 30 | 3. Run `nuxt prepare` and restart the tsserver. 31 | 32 | ## Features 33 | 34 | ### 1. components 35 | 36 | Update references when renaming auto imported component files. 37 | 38 | For example, when renaming `components/foo/bar.vue` to `components/foo/baz.vue`, all usages of `` will be updated to ``. 39 | 40 | It only works when the dev server is active. 41 | 42 | ### 2. importGlob 43 | 44 | Go to definition for dynamic imports with glob patterns. 45 | 46 | ```ts 47 | import(`~/assets/${name}.webp`); 48 | // ^^^^^^^^^^^^^^^^^^^^^^^ 49 | import.meta.glob("~/assets/*.webp"); 50 | // ^^^^^^^^^^^^^^^^^ 51 | ``` 52 | 53 | ### 3. nitroRoutes 54 | 55 | Go to definition for nitro routes in data fetching methods. 56 | 57 | ```ts 58 | useFetch("/api/foo"); 59 | // ^^^^^^^^^^ 60 | // Also `$fetch` and `useLazyFetch`. 61 | ``` 62 | 63 | It will fallback to resolve the URL from your `public` directory when no nitro routes match. 64 | 65 | ### 4. pageMeta 66 | 67 | Go to definition for page metadata. 68 | 69 | ```ts 70 | definePageMeta({ 71 | layout: "admin", 72 | // ^^^^^^^ 73 | middleware: ["auth"], 74 | // ^^^^^^ 75 | }); 76 | ``` 77 | 78 | ### 5. runtimeConfig 79 | 80 | Go to definition for runtime config. 81 | 82 | ```vue 83 | 87 | ``` 88 | 89 | ### 6. typedPages 90 | 91 | Go to definition for typed pages. 92 | 93 | ```vue 94 | 98 | ``` 99 | 100 | It can be triggered on the `name` property of an object literal constrained by the `RouteLocationRaw` type. 101 | 102 | ### 7. unimport 103 | 104 | Find references for SFC on `