From 4f75b4a89e7103d0d97bb876ff7d048cecdd5bb8 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Sat, 6 Dec 2025 00:39:48 +0800 Subject: [PATCH 1/4] fix: remove import specifiers with correct end offset --- src/plugins/hydration.ts | 45 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/plugins/hydration.ts b/src/plugins/hydration.ts index 5901e6b..71ce332 100644 --- a/src/plugins/hydration.ts +++ b/src/plugins/hydration.ts @@ -1,6 +1,6 @@ import { genImport } from 'knitwork' import MagicString from 'magic-string' -import { parseSync, type ImportDeclaration } from 'oxc-parser' +import { parseSync, type ImportDeclaration, type ImportDeclarationSpecifier, type ImportSpecifier } from 'oxc-parser' import { createUnplugin } from 'unplugin' const INCLUDE_VUE_RE = /\.vue$/ @@ -27,23 +27,21 @@ export const InjectHydrationPlugin = createUnplugin(() => { const hasDefineComponent = DEFINE_COMPONENT_RE.test(code) const hasDefineNuxtComponent = DEFINE_NUXT_COMPONENT_RE.test(code) - const defineComponentImport = findImportSpecifier(imports as ImportDeclaration[], 'defineComponent', ['vue', '#imports']) - const defineComponentAlias = defineComponentImport?.local.name || 'defineComponent' - if (defineComponentImport) { + const defineComponentImport = findImportSpecifier(imports, 'defineComponent', ['vue', '#imports'], (specifier, next) => { m.remove( - defineComponentImport.start, - defineComponentImport.end, + specifier.start, + next?.start ?? specifier.end, ) - } + }) + const defineComponentAlias = defineComponentImport?.local.name || 'defineComponent' - const defineNuxtComponentImport = findImportSpecifier(imports as ImportDeclaration[], 'defineNuxtComponent', ['#app/composables/component', '#imports', '#app', 'nuxt/app']) - const defineNuxtComponentAlias = defineNuxtComponentImport?.local.name || 'defineNuxtComponent' - if (defineNuxtComponentImport) { + const defineNuxtComponentImport = findImportSpecifier(imports, 'defineNuxtComponent', ['#app/composables/component', '#imports', '#app', 'nuxt/app'], (specifier, next) => { m.remove( - defineNuxtComponentImport.start, - defineNuxtComponentImport.end, + specifier.start, + next?.start ?? specifier.end, ) - } + }) + const defineNuxtComponentAlias = defineNuxtComponentImport?.local.name || 'defineNuxtComponent' const importsToAdd = new Set([ hasDefineComponent @@ -117,9 +115,22 @@ export const InjectHydrationPlugin = createUnplugin(() => { /** * Finds an import specifier for a given imported name from specified package names. */ -function findImportSpecifier(importDecl: ImportDeclaration[], importedName: string, pkgNames: string | string[]) { +function findImportSpecifier( + importDecl: ImportDeclaration[], + importedName: string, + pkgNames: string | string[], + callback?: (specifier: ImportSpecifier, next?: ImportDeclarationSpecifier) => void, +) { const names = Array.isArray(pkgNames) ? pkgNames : [pkgNames] - return importDecl.find(imp => names.includes(imp.source.value))?.specifiers.find((specifier) => { - return specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === importedName - }) + const decl = importDecl.find(imp => names.includes(imp.source.value)) + if (!decl) { + return + } + for (let i = 0; i < decl.specifiers.length; i++) { + const specifier = decl.specifiers[i]! + if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === importedName) { + callback?.(specifier, decl.specifiers[i + 1]) + return specifier + } + } } From 29ebb8c123ba4440180942d940b2ecf7959f2ad7 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 9 Dec 2025 00:22:53 +0800 Subject: [PATCH 2/4] chore: lint --- src/plugins/hydration.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/plugins/hydration.ts b/src/plugins/hydration.ts index 71ce332..68d1e17 100644 --- a/src/plugins/hydration.ts +++ b/src/plugins/hydration.ts @@ -27,20 +27,30 @@ export const InjectHydrationPlugin = createUnplugin(() => { const hasDefineComponent = DEFINE_COMPONENT_RE.test(code) const hasDefineNuxtComponent = DEFINE_NUXT_COMPONENT_RE.test(code) - const defineComponentImport = findImportSpecifier(imports, 'defineComponent', ['vue', '#imports'], (specifier, next) => { - m.remove( - specifier.start, - next?.start ?? specifier.end, - ) - }) + const defineComponentImport = findImportSpecifier( + imports, + 'defineComponent', + ['vue', '#imports'], + (specifier, nextSpecifier) => { + m.remove( + specifier.start, + nextSpecifier?.start ?? specifier.end, + ) + }, + ) const defineComponentAlias = defineComponentImport?.local.name || 'defineComponent' - const defineNuxtComponentImport = findImportSpecifier(imports, 'defineNuxtComponent', ['#app/composables/component', '#imports', '#app', 'nuxt/app'], (specifier, next) => { - m.remove( - specifier.start, - next?.start ?? specifier.end, - ) - }) + const defineNuxtComponentImport = findImportSpecifier( + imports, + 'defineNuxtComponent', + ['#app/composables/component', '#imports', '#app', 'nuxt/app'], + (specifier, next) => { + m.remove( + specifier.start, + next?.start ?? specifier.end, + ) + }, + ) const defineNuxtComponentAlias = defineNuxtComponentImport?.local.name || 'defineNuxtComponent' const importsToAdd = new Set([ @@ -119,7 +129,7 @@ function findImportSpecifier( importDecl: ImportDeclaration[], importedName: string, pkgNames: string | string[], - callback?: (specifier: ImportSpecifier, next?: ImportDeclarationSpecifier) => void, + callback?: (specifier: ImportSpecifier, nextSpecifier?: ImportDeclarationSpecifier) => void, ) { const names = Array.isArray(pkgNames) ? pkgNames : [pkgNames] const decl = importDecl.find(imp => names.includes(imp.source.value)) From e6f03efa20e6aabb3f1e6e1fbb10dc5f809e4fde Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 9 Dec 2025 00:28:38 +0800 Subject: [PATCH 3/4] test: add --- test/unit/hydration/vite-plugin.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unit/hydration/vite-plugin.test.ts b/test/unit/hydration/vite-plugin.test.ts index 458e00f..2e441be 100644 --- a/test/unit/hydration/vite-plugin.test.ts +++ b/test/unit/hydration/vite-plugin.test.ts @@ -92,6 +92,13 @@ describe('InjectHydrationPlugin', () => { expect(result.code).not.toContain('import { defineNuxtComponent } from \'#imports\'') }) + it('should replace defineComponent with correct offsets', async () => { + const code = `import { defineComponent, createVNode } from '#imports'\n${exportDefineComponent}` + const result = await modifyImportPluginTransform(code, 'test.ts') + expect(result.code).toContain(importDefineComponent.trim()) + expect(result.code).toContain('import { createVNode } from \'#imports\'') + }) + it('should inject import if defineComponent is used but not imported', async () => { const code = `${exportDefineComponent}` const result = await modifyImportPluginTransform(code, 'test.ts') From 2cd03976627f55d35faa4837914068303c833638 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Tue, 9 Dec 2025 00:30:13 +0800 Subject: [PATCH 4/4] test: add prev import specifier --- test/unit/hydration/vite-plugin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/hydration/vite-plugin.test.ts b/test/unit/hydration/vite-plugin.test.ts index 2e441be..1cb3b49 100644 --- a/test/unit/hydration/vite-plugin.test.ts +++ b/test/unit/hydration/vite-plugin.test.ts @@ -93,10 +93,10 @@ describe('InjectHydrationPlugin', () => { }) it('should replace defineComponent with correct offsets', async () => { - const code = `import { defineComponent, createVNode } from '#imports'\n${exportDefineComponent}` + const code = `import { useModel, defineComponent, createVNode } from '#imports'\n${exportDefineComponent}` const result = await modifyImportPluginTransform(code, 'test.ts') expect(result.code).toContain(importDefineComponent.trim()) - expect(result.code).toContain('import { createVNode } from \'#imports\'') + expect(result.code).toContain('import { useModel, createVNode } from \'#imports\'') }) it('should inject import if defineComponent is used but not imported', async () => {