-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
/
loader.ts
133 lines (117 loc) · 5.62 KB
/
loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { createUnplugin } from 'unplugin'
import { genDynamicImport, genImport } from 'knitwork'
import MagicString from 'magic-string'
import { pascalCase } from 'scule'
import { resolve } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit'
import { distDir } from '../dirs'
import { isVue } from '../core/utils'
interface LoaderOptions {
getComponents (): Component[]
mode: 'server' | 'client'
sourcemap?: boolean
transform?: ComponentsOptions['transform']
experimentalComponentIslands?: boolean
}
export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
const exclude = options.transform?.exclude || []
const include = options.transform?.include || []
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
const clientDelayedComponentRuntime = resolve(distDir, 'components/runtime/client-delayed-component')
return {
name: 'nuxt:components-loader',
enforce: 'post',
transformInclude (id) {
if (exclude.some(pattern => pattern.test(id))) {
return false
}
if (include.some(pattern => pattern.test(id))) {
return true
}
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
},
transform (code) {
const components = options.getComponents()
let num = 0
const imports = new Set<string>()
const map = new Map<Component, string>()
const s = new MagicString(code)
// replace `_resolveComponent("...")` to direct import
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?([^'"]*?)["'][\s,]*[^)]*\)/g, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode)
if (component) {
// @ts-expect-error TODO: refactor to nuxi
if (component._internal_install && tryUseNuxt()?.options.test === false) {
// @ts-expect-error TODO: refactor to nuxi
import('../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install))
}
let identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier)
const isServerOnly = !component._raw && component.mode === 'server' &&
!components.some(c => c.pascalName === component.pascalName && c.mode === 'client')
if (isServerOnly) {
imports.add(genImport(serverComponentRuntime, [{ name: 'createServerComponent' }]))
imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(name)})`)
if (!options.experimentalComponentIslands) {
logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`)
}
return identifier
}
const isClientOnly = !component._raw && component.mode === 'client'
if (isClientOnly) {
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
identifier += '_client'
}
if (lazy) {
// Temporary hardcoded check to verify runtime functionality
if (name === 'DelayedWrapperTestComponent') {
imports.add(genImport('vue', [{ name: 'defineAsyncComponent', as: '__defineAsyncComponent' }]))
imports.add(genImport(clientDelayedComponentRuntime, [{ name: 'createLazyIOClientPage' }]))
identifier += '_delayedIO'
imports.add(`const ${identifier} = createLazyIOClientPage(__defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)))`)
} else {
imports.add(genImport('vue', [{ name: 'defineAsyncComponent', as: '__defineAsyncComponent' }]))
identifier += '_lazy'
imports.add(`const ${identifier} = __defineAsyncComponent(${genDynamicImport(component.filePath, { interopDefault: false })}.then(c => c.${component.export ?? 'default'} || c)${isClientOnly ? '.then(c => createClientOnly(c))' : ''})`)
}
} else {
imports.add(genImport(component.filePath, [{ name: component._raw ? 'default' : component.export, as: identifier }]))
if (isClientOnly) {
imports.add(`const ${identifier}_wrapped = createClientOnly(${identifier})`)
identifier += '_wrapped'
}
}
return identifier
}
// no matched
return full
})
if (imports.size) {
s.prepend([...imports, ''].join('\n'))
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ hires: true })
: undefined,
}
}
},
}
})
function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) {
const id = pascalCase(name).replace(/["']/g, '')
// Prefer exact match
const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode))
if (component) { return component }
const otherModeComponent = components.find(component => id === component.pascalName)
// Render client-only components on the server with <ServerPlaceholder> (a simple div)
if (mode === 'server' && otherModeComponent) {
return components.find(c => c.pascalName === 'ServerPlaceholder')
}
// Return the other-mode component in all other cases - we'll handle createClientOnly
// and createServerComponent above
return otherModeComponent
}