Skip to content
This repository was archived by the owner on Apr 6, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
478dd5d
feat: client only component
antfu Nov 11, 2021
aac32ea
chore: add plugin
antfu Nov 11, 2021
a11dc8f
chore: update
antfu Nov 11, 2021
49f6e76
chore: update
antfu Nov 11, 2021
09d91ba
chore: update
antfu Nov 11, 2021
c260949
Merge branch 'main' into feat/client-only
antfu Nov 14, 2021
8090476
wip: update
antfu Nov 14, 2021
bb94b88
feat: working!
antfu Nov 14, 2021
b5ba76b
feat: passing slots and attrs
antfu Nov 14, 2021
0155358
chore: update example
antfu Nov 14, 2021
654fa71
Merge branch 'main' into feat/client-server-components
antfu Nov 15, 2021
3f29762
Merge branch 'main' into feat/client-server-components
antfu Nov 18, 2021
5463607
docs: add more comments
antfu Nov 18, 2021
413de89
chore: update
antfu Nov 18, 2021
adfaef8
chore: typo
Nov 22, 2021
8f1f499
Merge branch 'main' into feat/client-server-components
antfu Dec 2, 2021
aebd09d
wip: use redirecting for client/server
antfu Dec 2, 2021
d093fa9
feat: it works!
antfu Dec 2, 2021
1531aa4
chore: clean up
antfu Dec 2, 2021
0d60a37
Merge branch 'main' into feat/client-server-components
antfu Dec 11, 2021
e395a68
Merge branch 'main' into feat/client-server-components
antfu Dec 21, 2021
040e8f9
Update packages/nuxt3/src/app/components/client-only.mjs
antfu Dec 22, 2021
320b725
Merge branch 'main' into feat/client-server-components
antfu Dec 24, 2021
664f2f0
Merge branch 'main' into feat/client-server-components
pi0 Jan 11, 2022
808dc2e
Merge branch 'main' into feat/client-server-components
antfu Jan 21, 2022
2aea80c
Merge branch 'main' into feat/client-server-components
antfu Feb 7, 2022
827cd53
Merge branch 'feat/client-server-components' of https://github.com/nu…
antfu Feb 7, 2022
6a54ea6
chore: update
antfu Feb 7, 2022
98d4859
chore: cleanup
antfu Feb 7, 2022
1ef0dce
Merge branch 'main' into feat/client-server-components
pi0 Feb 7, 2022
05ea027
Merge branch 'main' into feat/client-server-components
antfu Mar 10, 2022
3e78d4f
chore: update
antfu Mar 10, 2022
399f944
Merge branch 'main' into feat/client-server-components
pi0 Mar 16, 2022
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
6 changes: 5 additions & 1 deletion examples/with-components/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<!-- Auto Imported Components -->
<HelloWorld class="text-2xl" />
<Nuxt3 class="text-2xl" />
<ParentFolderHello class="mt-6" />
<ParentFolderHello class="my-6" />
<ClientAndServer style="color: red">
<div>[Slot]</div>
</ClientAndServer>
<JustClient />
</div>

<template #tips>
Expand Down
10 changes: 10 additions & 0 deletions examples/with-components/components/ClientAndServer.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
const width = window.innerWidth
</script>

<template>
<div>
Window width: {{ width }}
<slot />
</div>
</template>
10 changes: 10 additions & 0 deletions examples/with-components/components/ClientAndServer.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div>
Loading width... (server fallback)
<slot />
</div>
</template>

<script setup>
console.log('Hi from Server Component!')
</script>
10 changes: 10 additions & 0 deletions examples/with-components/components/JustClient.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
const height = window.innerHeight
</script>

<template>
<div>
This is client only.
Window height: {{ height }}
</div>
</template>
18 changes: 17 additions & 1 deletion packages/nuxt3/src/app/components/client-only.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ref, onMounted, defineComponent, createElementBlock } from 'vue'
import { ref, onMounted, defineComponent, createElementBlock, h } from 'vue'

export default defineComponent({
name: 'ClientOnly',
Expand All @@ -17,3 +17,19 @@ export default defineComponent({
}
}
})

export function wrapClientOnly (component, mode) {
return defineComponent({
name: 'ClientOnlyWrapper',
setup (props, { attrs, slots }) {
const mounted = ref(false)
onMounted(() => { mounted.value = true })
return () => {
if (mounted.value === (mode !== 'server')) {
return h(component, { props, attrs }, slots)
}
return h('div')
}
}
})
}
8 changes: 8 additions & 0 deletions packages/nuxt3/src/app/components/nuxt-empty.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineComponent, createElementBlock } from 'vue'

export default defineComponent({
name: 'Empty',
render () {
return createElementBlock('div')
}
})
17 changes: 14 additions & 3 deletions packages/nuxt3/src/components/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { parseQuery, parseURL } from 'ufo'
import { Component } from '@nuxt/schema'
import { genImport } from 'knitwork'
import MagicString from 'magic-string'
import { getComponentPath } from './templates'

interface LoaderOptions {
getComponents(): Component[]
mode: 'server' | 'client'
}

export const loaderPlugin = createUnplugin((options: LoaderOptions) => ({
Expand All @@ -20,33 +22,42 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => ({
return pathname.endsWith('.vue') && (query.type === 'template' || !search)
},
transform (code, id) {
return transform(code, id, options.getComponents())
return transform(code, id, options.getComponents(), options.mode)
}
}))

function findComponent (components: Component[], name:string) {
return components.find(({ pascalName, kebabName }) => [pascalName, kebabName].includes(name))
}

function transform (code: string, id: string, components: Component[]) {
function transform (code: string, id: string, components: Component[], mode: 'server' | 'client') {
let num = 0
let imports = ''
const map = new Map<Component, string>()
const s = new MagicString(code)
let hasEnvComponents = false

// replace `_resolveComponent("...")` to direct import
s.replace(/ _resolveComponent\("(.*?)"\)/g, (full, name) => {
const component = findComponent(components, name)
if (component) {
const identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier)
imports += genImport(component.filePath, [{ name: component.export, as: identifier }])
imports += genImport(getComponentPath(component, mode), [{ name: component.export, as: identifier }])
if (component.envPaths) {
hasEnvComponents = true
return ` wrapClientOnly(${identifier}, '${mode}')`
}
return ` ${identifier}`
}
// no matched
return full
})

if (hasEnvComponents) {
imports = 'import { wrapClientOnly } from \'#app/components/client-only\';' + imports
}

if (imports) {
s.prepend(imports + '\n')
}
Expand Down
33 changes: 26 additions & 7 deletions packages/nuxt3/src/components/module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { statSync } from 'fs'
import { resolve, basename } from 'pathe'
import { defineNuxtModule, resolveAlias, addVitePlugin, addWebpackPlugin, addTemplate, addPluginTemplate } from '@nuxt/kit'
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
import { componentsTemplate, componentsTypeTemplate } from './templates'
import { componentsTypeTemplate, componentsClientTemplate, componentsServerTemplate } from './templates'
import { scanComponents } from './scan'
import { loaderPlugin } from './loader'

Expand Down Expand Up @@ -101,9 +101,12 @@ export default defineNuxtModule<ComponentsOptions>({
...componentsTypeTemplate,
options
})

addPluginTemplate({
...componentsTemplate,
...componentsClientTemplate,
options
})
addPluginTemplate({
...componentsServerTemplate,
options
})

Expand All @@ -128,8 +131,24 @@ export default defineNuxtModule<ComponentsOptions>({
}
})

const loaderOptions = { getComponents: () => options.components }
addWebpackPlugin(loaderPlugin.webpack(loaderOptions))
addVitePlugin(loaderPlugin.vite(loaderOptions))
const getComponents = () => options.components

nuxt.hook('vite:extendConfig', (config, { isClient }) => {
config.plugins = config.plugins || []
config.plugins.push(loaderPlugin.vite({
getComponents,
mode: isClient ? 'client' : 'server'
}))
})

nuxt.hook('webpack:config', (configs) => {
configs.forEach((config) => {
config.plugins = config.plugins || []
config.plugins.push(loaderPlugin.webpack({
getComponents,
mode: config.name === 'client' ? 'client' : 'server'
}))
})
})
}
})
63 changes: 46 additions & 17 deletions packages/nuxt3/src/components/scan.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { basename, extname, join, dirname, relative } from 'pathe'
import { globby } from 'globby'
import { pascalCase, splitByCase } from 'scule'
import type { Component, ComponentsDir } from '@nuxt/schema'
import type { ScanDir, Component, ComponentsDir, ComponentEnv } from '@nuxt/schema'
import { isIgnored } from '@nuxt/kit'

export function sortDirsByPathLength ({ path: pathA }: ScanDir, { path: pathB }: ScanDir): number {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
}

// vue@2 src/shared/util.js
// TODO: update to vue3?
function hyphenate (str: string): string {
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase()
}

function resolveEnvComponent (fileName:string) {
const match = fileName.match(/^(.+)\.(client|server)$/)
if (match) {
return {
fileName: match[1],
env: match[2] as ComponentEnv
}
} else {
return {
fileName,
env: undefined
}
}
}

/**
* Scan the components inside different components folders
* and return a unique list of components
Expand All @@ -28,10 +47,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
// All scanned paths
const scannedPaths: string[] = []

for (const dir of dirs) {
// A map from resolved path to component name (used for making duplicate warning message)
const resolvedNames = new Map<string, string>()

for (const dir of dirs.sort(sortDirsByPathLength)) {
for (const _file of await globby(dir.pattern!, { cwd: dir.path, ignore: dir.ignore })) {
const filePath = join(dir.path, _file)

Expand All @@ -55,6 +71,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
dir.prefix ? splitByCase(dir.prefix) : [],
(dir.pathPrefix !== false) ? splitByCase(relative(dir.path, dirname(filePath))) : []
)
let env: ComponentEnv | undefined

/**
* In case we have index as filename the component become the parent path
Expand All @@ -68,6 +85,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
}
// eslint-disable-next-line prefer-const
({ env, fileName } = resolveEnvComponent(fileName))

/**
* Array of fileName parts splitted by case, / or -
Expand All @@ -86,16 +105,6 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
}

const componentName = pascalCase(componentNameParts) + pascalCase(fileNameParts)

if (resolvedNames.has(componentName)) {
console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
`\n - ${filePath}` +
`\n - ${resolvedNames.get(componentName)}`
)
continue
}
resolvedNames.set(componentName, filePath)

const pascalName = pascalCase(componentName).replace(/["']/g, '')
const kebabName = hyphenate(componentName)
const shortPath = relative(srcDir, filePath)
Expand All @@ -113,13 +122,33 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
preload: Boolean(dir.preload)
}

if (env) {
component.envPaths = {
[env]: filePath
}
}

if (typeof dir.extendComponent === 'function') {
component = (await dir.extendComponent(component)) || component
}

// Ignore component if component is already defined
if (!components.find(c => c.pascalName === component.pascalName)) {
// Check if component is already defined
const definedComponent = components.find(c => c.pascalName === component.pascalName)
if (!definedComponent) {
// Not defined, add component
components.push(component)
} else if (component.level < definedComponent.level) {
// Overwite if level is inferiour
Object.assign(definedComponent, component)
} else if (definedComponent.envPaths && component.envPaths) {
// Merge client and server component path
Object.assign(definedComponent.envPaths, component.envPaths)
} else {
// Naming conflict warning, ignore the later one
console.warn(`Two component files resolving to the same name \`${componentName}\`:\n` +
`\n - ${filePath}` +
`\n - ${definedComponent.filePath}`
)
}
}
scannedPaths.push(dir.path)
Expand Down
57 changes: 47 additions & 10 deletions packages/nuxt3/src/components/templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { isAbsolute, join, relative } from 'pathe'
import type { Component } from '@nuxt/schema'
import { relative, isAbsolute, join } from 'pathe'
import type { Component, NuxtPlugin } from '@nuxt/schema'
import { genDynamicImport, genObjectFromRawEntries } from 'knitwork'

export type ComponentsTemplateOptions = {
Expand All @@ -23,16 +23,29 @@ const createImportMagicComments = (options: ImportMagicCommentsOptions) => {
].filter(Boolean).join(', ')
}

export const componentsTemplate = {
filename: 'components.mjs',
getContents ({ options }: { options: ComponentsTemplateOptions }) {
return `import { defineAsyncComponent } from 'vue'
export function getComponentPath (component: Component, mode?: NuxtPlugin['mode']) {
if (!component.envPaths || !mode || mode === 'all') {
return component.filePath
}
const envPath = mode === 'client'
? component.envPaths.client
: component.envPaths.server
return envPath || '#app/components/nuxt-empty'
}

function getComponentTemplate (components: Component[], mode: NuxtPlugin['mode']) {
return `import { defineAsyncComponent, h as __h } from 'vue'
import { wrapClientOnly } from '#app/components/client-only'

const components = ${genObjectFromRawEntries(options.components.filter(c => c.global === true).map((c) => {
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
const components = ${genObjectFromRawEntries(components.filter(c => c.global === true).map((c) => {
let exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
const comment = createImportMagicComments(c)
const path = getComponentPath(c, mode)
if (c.envPaths) {
exp = `wrapClientOnly(${exp}, '${mode}')`
}

return [c.pascalName, `defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`]
return [c.pascalName, `defineAsyncComponent(${genDynamicImport(path, { comment })}.then(c => ${exp}))`]
}))}

export default function (nuxtApp) {
Expand All @@ -42,6 +55,21 @@ export default function (nuxtApp) {
}
}
`
}

export const componentsClientTemplate = {
filename: 'components-client.mjs',
mode: 'client' as const,
getContents ({ options }: { options: ComponentsTemplateOptions }) {
return getComponentTemplate(options.components, 'client')
}
}

export const componentsServerTemplate = {
filename: 'components-server.mjs',
mode: 'server' as const,
getContents ({ options }: { options: ComponentsTemplateOptions }) {
return getComponentTemplate(options.components, 'server')
}
}

Expand All @@ -50,7 +78,16 @@ export const componentsTypeTemplate = {
getContents: ({ options }: { options: ComponentsTemplateOptions }) => `// Generated by components discovery
declare module 'vue' {
export interface GlobalComponents {
${options.components.map(c => ` '${c.pascalName}': typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(join(options.buildDir, 'types'), c.filePath) : c.filePath, { wrapper: false })}['${c.export}']`).join(',\n')}
${options.components
.map((c) => {
const filePath = c.envPaths?.client || c.filePath
const dynamicImport = genDynamicImport(isAbsolute(filePath)
? relative(join(options.buildDir, 'types'), filePath)
: filePath, { wrapper: false })
return ` '${c.pascalName}': typeof ${dynamicImport}['${c.export}']`
})
.join(',\n')
}
}
}
export {}
Expand Down
Loading