Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/plugins/lazy-load.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { genImport } from 'knitwork'
import MagicString from 'magic-string'
import { basename, resolve } from 'node:path'
import { parse, resolve, } from 'node:path'

Check failure on line 3 in src/plugins/lazy-load.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected trailing comma
import { parseSync, type CallExpression, type ImportDeclaration, type ImportDefaultSpecifier, type ImportSpecifier } from 'oxc-parser'
import { createUnplugin } from 'unplugin'
import { distDir } from '../dirs'
import { findDefineComponentCalls } from './utils'

import { useNuxt } from '@nuxt/kit'
import type { Component } from '@nuxt/schema'

Check failure on line 9 in src/plugins/lazy-load.ts

View workflow job for this annotation

GitHub Actions / ci

Expected 1 empty line after import statement not followed by another import
const INCLUDE_FILES = /\.(vue|tsx?|jsx?)$/
// Exclude node_moduels as users can have control over it
const EXCLUDE_NODE_MODULES = /node_modules/
const skipPath = normalizePath(resolve(distDir, 'runtime/lazy-load'))

export const LazyLoadHintPlugin = createUnplugin(() => {

Check failure on line 15 in src/plugins/lazy-load.ts

View workflow job for this annotation

GitHub Actions / ci

Block must not be padded by blank lines

const nuxt = useNuxt()

let nuxtComponents: Component[] = nuxt.apps.default!.components
nuxt.hook('components:extend', (extendedComponents) => {
nuxtComponents = extendedComponents
})

return {
name: '@nuxt/hints:lazy-load-plugin',
enforce: 'post',
Expand Down Expand Up @@ -77,8 +86,9 @@
const wrapperStatements = directComponentImports
.map((imp) => {
const originalName = `__original_${imp.name}`
const component = nuxtComponents.find(c => c.filePath === imp.source)
// Rename the import to __original_X and create a wrapped version as X
return `const ${imp.name} = __wrapImportedComponent(${originalName}, '${imp.name}', '${imp.source}', '${normalizePath(id)}')`
return `const ${imp.name} = __wrapImportedComponent(${originalName}, '${component ? component.pascalName : imp.name}', '${imp.source}', '${normalizePath(id)}')`
})
.join('\n')

Expand Down Expand Up @@ -120,7 +130,7 @@
if (imp.name.startsWith('__nuxt')) {
// Auto imported components are using __nuxt_component_
// See nuxt loadeer plugin
return `{ componentName: '${basename(imp.source)}', importSource: '${imp.source}', importedBy: '${normalizePath(id)}', rendered: false }`
return `{ componentName: '${parse(imp.source).name}', importSource: '${imp.source}', importedBy: '${normalizePath(id)}', rendered: false }`
}
return `{ componentName: '${imp.name}', importSource: '${imp.source}', importedBy: '${normalizePath(id)}', rendered: false }`
}).join(', ')
Expand Down Expand Up @@ -173,7 +183,7 @@
// Inject useLazyComponentTracking call at the start of the setup function body
const insertPos = (setupFunc.body?.start ?? 0) + 1 // after {
const componentsArray = directComponentImports.map((imp) => {
const componentName = imp.name.startsWith('__nuxt') ? basename(imp.source) : imp.name
const componentName = imp.name.startsWith('__nuxt') ? parse(imp.source).name : imp.name
return `{ componentName: '${componentName}', importSource: '${imp.source}', importedBy: '${normalizePath(id)}', rendered: false }`
}).join(', ')
const injectionCode = `\nconst lazyHydrationState = useLazyComponentTracking([${componentsArray}]);\n`
Expand Down
105 changes: 101 additions & 4 deletions test/unit/hydration/lazy-hydration-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect } from 'vitest'
import { LazyLoadHintPlugin } from '../../../src/plugins/lazy-load'
import { describe, it, expect, vi, beforeEach } from 'vitest'

Check failure on line 2 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

'beforeEach' is defined but never used. Allowed unused vars must match /^_/u
import type { Plugin } from 'vite'
import type { ObjectHook } from 'unplugin'
import type { HookFnMap, ObjectHook, UnpluginBuildContext, UnpluginContext } from 'unplugin'
import { LazyLoadHintPlugin } from '../../../src/plugins/lazy-load'
import { useNuxt } from '@nuxt/kit'

vi.mock('@nuxt/kit', () => ({
useNuxt: vi.fn(() => ({
apps: {
default: {
components: [],
},
},
hook: vi.fn(),
})),
}))

const plugin = LazyLoadHintPlugin.vite() as Plugin
const transform = (plugin.transform as ObjectHook<any, any>).handler

const unpluginCtx: UnpluginBuildContext & UnpluginContext = {
addWatchFile:vi.fn(),

Check failure on line 22 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Missing space before value for key 'addWatchFile'
emitFile:vi.fn(),

Check failure on line 23 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Missing space before value for key 'emitFile'
getWatchFiles:vi.fn(),

Check failure on line 24 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Missing space before value for key 'getWatchFiles'
parse: vi.fn(),
getNativeBuildContext: vi.fn(),
error: vi.fn(),
warn : vi.fn(),

Check failure on line 28 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Extra space after key 'warn'
}
describe('LazyLoadHintPlugin', () => {
describe('default imports', () => {
it('should wrap a default import from a .vue file', async () => {
Expand Down Expand Up @@ -67,6 +87,83 @@
})
})

describe('skipped imports', () => {
it('should not transform type-only imports', async () => {
const code = `import type MyComp from './MyComp.vue'\nexport default {}`
const result = await transform(code, '/src/Parent.vue')
expect(result).toBeUndefined()
})

it('should return undefined when there are no .vue imports at all', async () => {
const code = `const x = 1\nexport default { x }`
const result = await transform(code, '/src/Parent.vue')
expect(result).toBeUndefined()
})
})

describe('nuxt auto-imported components (__nuxt prefix)', () => {
it('should use file basename as componentName for __nuxt prefixed imports in _sfc_main', async () => {
const code = [
`import __nuxt_component_0 from './ChildComp.vue'`,
`const _sfc_main = {}`,
`export default _sfc_main`,
].join('\n')
const result = await transform(code, '/src/Parent.vue')
expect(result.code).toContain(`componentName: 'ChildComp'`)
expect(result.code).toContain(`importSource: './ChildComp.vue'`)
})

it('should use file basename as componentName for __nuxt prefixed imports in defineComponent setup', async () => {
const code = [
`import { defineComponent } from 'vue'`,
`import __nuxt_component_0 from './SomeWidget.vue'`,
`export default defineComponent({`,
` setup() {`,
` return {}`,
` }`,
`})`,
].join('\n')
const result = await transform(code, '/src/Parent.ts')
expect(result.code).toContain(`componentName: 'SomeWidget'`)
expect(result.code).toContain('useLazyComponentTracking(')
})
})

describe('nuxtComponents matching (pascalName)', () => {
it('should use pascalName from nuxtComponents when component filePath matches', async () => {
vi.mocked(useNuxt).mockReturnValueOnce({
apps: {
// @ts-expect-error partial mock
default: {
components: [
{
filePath: './MyWidget.vue',
pascalName: 'MyWidgetPascal',
kebabName: '',
export: '',
shortPath: '',
chunkName: '',
prefetch: false,
preload: false

Check failure on line 147 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Missing trailing comma
},
],
},
},
hook: vi.fn(),
})

const { LazyLoadHintPlugin: PluginWithComponents } = await import('../../../src/plugins/lazy-load')
const p = PluginWithComponents.vite() as Plugin
const t = (p.transform as ObjectHook<HookFnMap['transform'], 'code'|'id'>).handler

Check failure on line 157 in test/unit/hydration/lazy-hydration-plugin.test.ts

View workflow job for this annotation

GitHub Actions / ci

Operator '|' must be spaced

const code = `import MyWidget from './MyWidget.vue'\nexport default { components: { MyWidget } }`
const result = await t.call(unpluginCtx, code, '/src/Parent.vue') as unknown as { code: string }
expect(result.code).toContain(
`__wrapImportedComponent(__original_MyWidget, 'MyWidgetPascal', './MyWidget.vue'`,
)
})
})

describe('defineComponent setup injection', () => {
it('should inject useLazyComponentTracking in defineComponent setup', async () => {
const code = [
Expand Down
Loading