├── .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 |
2 |
3 |
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 |
2 | About Page
3 |
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 |
8 | Contact Page
9 |
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 | ....
11 |
12 |
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 | [](https://www.npmjs.com/package/@dxup/unimport)
4 | [](https://www.npmjs.com/package/@dxup/unimport)
5 | [](/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 |
62 |
63 |
64 |
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 | [](https://www.npmjs.com/package/@dxup/nuxt)
4 | [](https://www.npmjs.com/package/@dxup/nuxt)
5 | [](/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 |
84 | {{ $config.public.domain }}
85 |
86 |
87 | ```
88 |
89 | ### 6. typedPages
90 |
91 | Go to definition for typed pages.
92 |
93 | ```vue
94 |
95 |
96 |
97 |
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 ``.
105 |
106 | ```vue
107 | ....
108 |
109 |
110 | ```
111 |
112 | Please refer to the [@dxup/unimport](/packages/unimport) package for more details.
113 |
--------------------------------------------------------------------------------
/test/__snapshots__/playground.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`playground > app/app.vue > auto imports > definition 1`] = `
4 | [
5 | {
6 | "fileName": "app/utils/index.ts",
7 | "textSpan": {
8 | "length": 3,
9 | "start": 63,
10 | },
11 | },
12 | ]
13 | `;
14 |
15 | exports[`playground > app/app.vue > import glob > definition 1`] = `
16 | [
17 | {
18 | "fileName": "app/assets/maestrale.webp",
19 | "textSpan": {
20 | "length": 0,
21 | "start": 0,
22 | },
23 | },
24 | {
25 | "fileName": "app/assets/libeccio.webp",
26 | "textSpan": {
27 | "length": 0,
28 | "start": 0,
29 | },
30 | },
31 | ]
32 | `;
33 |
34 | exports[`playground > app/app.vue > import glob > definition 2`] = `
35 | [
36 | {
37 | "fileName": "app/assets/maestrale.webp",
38 | "textSpan": {
39 | "length": 0,
40 | "start": 0,
41 | },
42 | },
43 | {
44 | "fileName": "app/assets/libeccio.webp",
45 | "textSpan": {
46 | "length": 0,
47 | "start": 0,
48 | },
49 | },
50 | ]
51 | `;
52 |
53 | exports[`playground > app/app.vue > nitro routes > definition 1`] = `
54 | [
55 | {
56 | "fileName": "server/routes/sitemap.post.ts",
57 | "textSpan": {
58 | "length": 0,
59 | "start": 0,
60 | },
61 | },
62 | {
63 | "fileName": "server/routes/sitemap.get.ts",
64 | "textSpan": {
65 | "length": 0,
66 | "start": 0,
67 | },
68 | },
69 | ]
70 | `;
71 |
72 | exports[`playground > app/app.vue > nitro routes > definition 2`] = `
73 | [
74 | {
75 | "fileName": "public/fallback.json",
76 | "textSpan": {
77 | "length": 0,
78 | "start": 0,
79 | },
80 | },
81 | ]
82 | `;
83 |
84 | exports[`playground > app/app.vue > nitro routes > definition 3`] = `
85 | [
86 | {
87 | "fileName": "server/api/foo.get.ts",
88 | "textSpan": {
89 | "length": 0,
90 | "start": 0,
91 | },
92 | },
93 | ]
94 | `;
95 |
96 | exports[`playground > app/app.vue > nitro routes > definition 4`] = `
97 | [
98 | {
99 | "fileName": "server/api/foo.post.ts",
100 | "textSpan": {
101 | "length": 0,
102 | "start": 0,
103 | },
104 | },
105 | ]
106 | `;
107 |
108 | exports[`playground > app/app.vue > page meta > definition 1`] = `
109 | [
110 | {
111 | "fileName": "app/layouts/center.vue",
112 | "textSpan": {
113 | "length": 0,
114 | "start": 0,
115 | },
116 | },
117 | ]
118 | `;
119 |
120 | exports[`playground > app/app.vue > page meta > definition 2`] = `
121 | [
122 | {
123 | "fileName": "app/middleware/auth.ts",
124 | "textSpan": {
125 | "length": 0,
126 | "start": 0,
127 | },
128 | },
129 | ]
130 | `;
131 |
132 | exports[`playground > app/app.vue > runtime config > definition 1`] = `
133 | [
134 | {
135 | "fileName": "layers/archer/nuxt.config.ts",
136 | "textSpan": {
137 | "length": 3,
138 | "start": 82,
139 | },
140 | },
141 | {
142 | "fileName": "layers/berserker/nuxt.config.ts",
143 | "textSpan": {
144 | "length": 3,
145 | "start": 82,
146 | },
147 | },
148 | {
149 | "fileName": "nuxt.config.ts",
150 | "textSpan": {
151 | "length": 3,
152 | "start": 43,
153 | },
154 | },
155 | ]
156 | `;
157 |
158 | exports[`playground > app/app.vue > runtime config > definition 2`] = `
159 | [
160 | {
161 | "fileName": "layers/berserker/nuxt.config.ts",
162 | "textSpan": {
163 | "length": 3,
164 | "start": 102,
165 | },
166 | },
167 | {
168 | "fileName": "nuxt.config.ts",
169 | "textSpan": {
170 | "length": 3,
171 | "start": 59,
172 | },
173 | },
174 | ]
175 | `;
176 |
177 | exports[`playground > app/app.vue > runtime config > definition 3`] = `
178 | [
179 | {
180 | "fileName": "nuxt.config.ts",
181 | "textSpan": {
182 | "length": 3,
183 | "start": 75,
184 | },
185 | },
186 | ]
187 | `;
188 |
189 | exports[`playground > app/app.vue > runtime config > definition 4`] = `
190 | [
191 | {
192 | "fileName": "nuxt.config.ts",
193 | "textSpan": {
194 | "length": 5,
195 | "start": 112,
196 | },
197 | },
198 | ]
199 | `;
200 |
201 | exports[`playground > app/app.vue > typed pages > definition 1`] = `
202 | [
203 | {
204 | "fileName": "app/pages/about.vue",
205 | "textSpan": {
206 | "length": 0,
207 | "start": 0,
208 | },
209 | },
210 | ]
211 | `;
212 |
213 | exports[`playground > app/app.vue > typed pages > definition 2`] = `
214 | [
215 | {
216 | "fileName": "app/pages/about/contact.vue",
217 | "textSpan": {
218 | "length": 0,
219 | "start": 0,
220 | },
221 | },
222 | ]
223 | `;
224 |
225 | exports[`playground > app/app.vue > typed pages > definition 3`] = `
226 | [
227 | {
228 | "fileName": "app/pages/about.vue",
229 | "textSpan": {
230 | "length": 0,
231 | "start": 0,
232 | },
233 | },
234 | ]
235 | `;
236 |
237 | exports[`playground > app/components/foo-bar.vue > unimport > references 1`] = `
238 | [
239 | {
240 | "fileName": "app/components/foo-bar.vue",
241 | "textSpan": {
242 | "length": 6,
243 | "start": 38,
244 | },
245 | },
246 | {
247 | "fileName": "app/components/foo-bar.vue",
248 | "textSpan": {
249 | "length": 6,
250 | "start": 77,
251 | },
252 | },
253 | {
254 | "fileName": "app/app.vue",
255 | "textSpan": {
256 | "length": 6,
257 | "start": 1814,
258 | },
259 | },
260 | {
261 | "fileName": "app/app.vue",
262 | "textSpan": {
263 | "length": 12,
264 | "start": 1829,
265 | },
266 | },
267 | ]
268 | `;
269 |
270 | exports[`playground > app/utils/index.ts > auto imports > references 1`] = `
271 | [
272 | {
273 | "fileName": "app/app.vue",
274 | "textSpan": {
275 | "length": 3,
276 | "start": 755,
277 | },
278 | },
279 | ]
280 | `;
281 |
--------------------------------------------------------------------------------
/packages/nuxt/src/module/index.ts:
--------------------------------------------------------------------------------
1 | import { addTemplate, defineNuxtModule, useNitro } from "@nuxt/kit";
2 | import * as packageJson from "../../package.json";
3 | import { createEventClient } from "../event/client";
4 | import { onComponentsRename } from "./events";
5 | import type { Data } from "../typescript/types";
6 |
7 | interface Plugin {
8 | name: string;
9 | options?: Record;
10 | }
11 |
12 | export interface ModuleOptions {
13 | features?: {
14 | /**
15 | * Whether to update references when renaming auto imported component files.
16 | * @default true
17 | */
18 | components?: boolean;
19 | /**
20 | * Whether to enable Go to Definition for dynamic imports with glob patterns.
21 | * @default true
22 | */
23 | importGlob?: boolean;
24 | /**
25 | * Whether to enable Go to Definition for nitro routes in data fetching methods.
26 | * @default true
27 | */
28 | nitroRoutes?: boolean;
29 | /**
30 | * Whether to enable Go to Definition for page metadata.
31 | * @default true
32 | */
33 | pageMeta?: boolean;
34 | /**
35 | * Whether to enable Go to Definition for runtime config.
36 | * @default true
37 | */
38 | runtimeConfig?: boolean;
39 | /**
40 | * Whether to enable Go to Definition for typed pages.
41 | * @default true
42 | */
43 | typedPages?: boolean;
44 | /**
45 | * Whether to enable enhanced navigation for auto imported APIs.
46 | * @default true
47 | */
48 | unimport?: boolean | {
49 | /**
50 | * Whether to enable Find References for SFC on ``.
51 | */
52 | componentReferences: boolean;
53 | };
54 | };
55 | }
56 |
57 | export default defineNuxtModule().with({
58 | meta: {
59 | name: packageJson.name,
60 | configKey: "dxup",
61 | },
62 | defaults: {
63 | features: {
64 | components: true,
65 | importGlob: true,
66 | nitroRoutes: true,
67 | pageMeta: true,
68 | runtimeConfig: true,
69 | typedPages: true,
70 | unimport: true,
71 | },
72 | },
73 | async setup(options, nuxt) {
74 | const pluginsTs: Plugin[] = [{ name: "@dxup/nuxt" }];
75 |
76 | if (options.features?.unimport) {
77 | pluginsTs.unshift({ name: "@dxup/unimport" });
78 | }
79 |
80 | append(pluginsTs, nuxt.options, "typescript", "tsConfig", "compilerOptions");
81 | append(pluginsTs, nuxt.options.nitro, "typescript", "tsConfig", "compilerOptions");
82 | append(pluginsTs, nuxt.options, "typescript", "sharedTsConfig", "compilerOptions");
83 | append(pluginsTs, nuxt.options, "typescript", "nodeTsConfig", "compilerOptions");
84 |
85 | addTemplate({
86 | filename: "dxup/data.json",
87 | write: true,
88 | getContents({ nuxt, app }) {
89 | const layouts = Object.fromEntries(
90 | Object.values(app.layouts).map((item) => [item.name, item.file]),
91 | );
92 |
93 | const middleware = app.middleware.reduce((acc, item) => {
94 | if (!item.global) {
95 | acc[item.name] = item.path;
96 | }
97 | return acc;
98 | }, {} as Data["middleware"]);
99 |
100 | const nitro = useNitro();
101 | const nitroRoutes = nitro.scannedHandlers.reduce((acc, item) => {
102 | if (item.route && item.method) {
103 | (acc[item.route] ??= {})[item.method] = item.handler;
104 | }
105 | return acc;
106 | }, {} as Data["nitroRoutes"]);
107 |
108 | const typedPages = app.pages?.reduce(function reducer(acc, page) {
109 | if (page.name && page.file) {
110 | acc[page.name] = page.file;
111 | }
112 | if (page.children) {
113 | for (const child of page.children) {
114 | reducer(acc, child);
115 | }
116 | }
117 | return acc;
118 | }, {} as Data["typedPages"]);
119 |
120 | const data = {
121 | buildDir: nuxt.options.buildDir,
122 | publicDir: nuxt.options.dir.public,
123 | configFiles: [
124 | ...nuxt.options._nuxtConfigFiles,
125 | ...nuxt.options._layers.map((layer) => layer._configFile).filter(Boolean),
126 | ],
127 | layouts,
128 | middleware,
129 | nitroRoutes,
130 | typedPages,
131 | features: {
132 | ...options.features,
133 | unimport: {
134 | componentReferences: typeof options.features.unimport === "object"
135 | ? options.features.unimport.componentReferences
136 | : options.features.unimport,
137 | },
138 | },
139 | };
140 | return JSON.stringify(data, null, 2);
141 | },
142 | });
143 |
144 | if (nuxt.options.dev) {
145 | const client = await createEventClient(nuxt);
146 | client.on("components:rename", (data) => onComponentsRename(nuxt, data));
147 | }
148 | },
149 | });
150 |
151 | function append<
152 | T extends Record,
153 | K0 extends keyof T,
154 | K1 extends keyof NonNullable,
155 | K2 extends keyof NonNullable[K1]>,
156 | >(plugins: any[], target: T, ...keys: [K0, K1?, K2?]) {
157 | for (const key of keys) {
158 | target = (target as any)[key] ??= {};
159 | }
160 | ((target as any).plugins ??= []).push(...plugins);
161 | }
162 |
--------------------------------------------------------------------------------
/test/playground.test.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { Buffer } from "node:buffer";
4 | import { relative, resolve } from "pathe";
5 | import ts from "typescript";
6 | import { describe, expect, it } from "vitest";
7 | import type { Language } from "@volar/language-core";
8 |
9 | describe("playground", async () => {
10 | const logger: ts.server.Logger = {
11 | close: () => {},
12 | endGroup: () => {},
13 | getLogFileName: () => void 0,
14 | hasLevel: () => false,
15 | info: () => {},
16 | loggingEnabled: () => false,
17 | msg: () => {},
18 | perftrc: () => {},
19 | startGroup: () => {},
20 | };
21 |
22 | const session = new ts.server.Session({
23 | byteLength: Buffer.byteLength,
24 | cancellationToken: ts.server.nullCancellationToken,
25 | canUseEvents: true,
26 | host: ts.sys as any,
27 | hrtime: process.hrtime,
28 | logger,
29 | useInferredProjectPerProjectRoot: false,
30 | useSingleInferredProject: false,
31 | });
32 |
33 | const projectService = new ts.server.ProjectService({
34 | cancellationToken: ts.server.nullCancellationToken,
35 | globalPlugins: [
36 | "@vue/typescript-plugin",
37 | ],
38 | host: ts.sys as any,
39 | logger,
40 | session,
41 | useInferredProjectPerProjectRoot: false,
42 | useSingleInferredProject: false,
43 | });
44 |
45 | const playgroundRoot = resolve(import.meta.dirname, "../playground");
46 | const appVuePath = resolve(playgroundRoot, "app/app.vue");
47 | const buildDir = resolve(playgroundRoot, ".nuxt");
48 | projectService.openClientFile(appVuePath);
49 |
50 | // wait for the postprocess of language service to complete
51 | await delay(0);
52 |
53 | const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(appVuePath), true)!;
54 | const languageService = project.getLanguageService();
55 | const program = languageService.getProgram()!;
56 | const language = (project as any).__vue__.language as Language;
57 |
58 | const scopeRE = /(?:\/\*|)/;
59 | const operationRE = /(?<=(?:\/\/|