Skip to content

Commit

Permalink
feat: introduce component-resolver transformer (#1907)
Browse files Browse the repository at this point in the history

Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
  • Loading branch information
farnabaz and Atinux committed Feb 20, 2023
1 parent f7beb8b commit 3f6efe8
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 6 deletions.
37 changes: 36 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
addTemplate,
extendViteConfig
} from '@nuxt/kit'
import { genImport, genSafeVariableName } from 'knitwork'
import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
import type { ListenOptions } from 'listhen'
// eslint-disable-next-line import/no-named-as-default
import defu from 'defu'
Expand All @@ -20,6 +20,7 @@ import { listen } from 'listhen'
import type { WatchEvent } from 'unstorage'
import { createStorage } from 'unstorage'
import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
import type { Component } from '@nuxt/schema'
import { name, version } from '../package.json'
import {
CACHE_VERSION,
Expand Down Expand Up @@ -413,6 +414,40 @@ export default defineNuxtModule<ModuleOptions>({
global: true
})

const componentsContext = { components: [] as Component[] }
nuxt.hook('components:extend', (newComponents) => {
componentsContext.components = newComponents.filter((c) => {
if (c.pascalName.startsWith('Prose') || c.pascalName === 'NuxtLink') {
return true
}

if (
c.filePath.includes('@nuxt/content/dist') ||
c.filePath.includes('nuxt/dist/app') ||
c.filePath.includes('NuxtWelcome')
) {
return false
}

return true
})
})
addTemplate({
filename: 'content-components.mjs',
getContents ({ options }) {
const components = options.getComponents(options.mode).filter((c: any) => !c.island).flatMap((c: any) => {
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
const isClient = c.mode === 'client'
const definitions = []

definitions.push(`export const ${c.pascalName} = ${genDynamicImport(c.filePath)}.then(c => ${isClient ? `createClientOnly(${exp})` : exp})`)
return definitions
})
return components.join('\n')
},
options: { getComponents: () => componentsContext.components }
})

const typesPath = addTemplate({
filename: 'types/content.d.ts',
getContents: () => [
Expand Down
13 changes: 8 additions & 5 deletions src/runtime/components/ContentRendererMarkdown.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { h, resolveComponent, Text, defineComponent } from 'vue'
import { h, resolveComponent, Text, defineComponent, toRaw } from 'vue'
import destr from 'destr'
import { pascalCase } from 'scule'
import { find, html } from 'property-information'
Expand Down Expand Up @@ -58,7 +58,7 @@ export default defineComponent({
await resolveContentComponents(props.value.body, {
tags: {
...tags,
...props.value?._components || {},
...toRaw(props.value?._components || {}),
...props.components
}
})
Expand All @@ -81,7 +81,7 @@ export default defineComponent({
...(value as ParsedContentMeta),
tags: {
...tags,
...value?._components || {},
...toRaw(value?._components || {}),
...components
}
}
Expand Down Expand Up @@ -316,9 +316,9 @@ function propsToDataRxBind (key: string, value: any, data: any, documentMeta: Pa
/**
* Resolve component if it's a Vue component
*/
const resolveVueComponent = (component: string) => {
const resolveVueComponent = (component: any) => {
// Check if node is not a native HTML tag
if (!htmlTags.includes(component as any)) {
if (!htmlTags.includes(component) && !component?.render) {
const componentFn = resolveComponent(pascalCase(component), false)
// If component exists
if (typeof componentFn === 'object') {
Expand Down Expand Up @@ -383,6 +383,9 @@ function mergeTextNodes (nodes: Array<VNode>) {
async function resolveContentComponents (body, meta) {
const components = Array.from(new Set(loadComponents(body, meta)))
await Promise.all(components.map(async (c) => {
if ((c as any)?.render) {
return
}
const resolvedComponent = resolveComponent(c) as any
if (resolvedComponent?.__asyncLoader && !resolvedComponent.__asyncResolved) {
await resolvedComponent.__asyncLoader()
Expand Down
51 changes: 51 additions & 0 deletions src/runtime/transformers/component-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { pascalCase } from 'scule'
import { ParsedContent } from '../types'
import htmlTags from '../utils/html-tags'
import { defineTransformer } from './utils'

async function resolveContentComponents (body: ParsedContent['body'], meta: Record<string, string>) {
const components = Array.from(new Set(loadComponents(body, meta)))
// @ts-ignore
const manifest = await import('#build/content-components').catch(() => ({}))
const resolvedComponentsEntries = await Promise.all(components.map(async ([t, c]) => {
const componentImporter = manifest[pascalCase(c)]
if (typeof componentImporter === 'function') {
return [t, await componentImporter()]
}
return [t, c]
}))

return Object.fromEntries(resolvedComponentsEntries)
function loadComponents (node: any, tags: Record<string, string>) {
if (node.type === 'text' || node.tag === 'binding') {
return []
}
const renderTag: string = (typeof node.props?.__ignoreMap === 'undefined' && tags[node.tag!]) || node.tag!
const components: Array<Array<string>> = []
if (node.type !== 'root' && !htmlTags.includes(renderTag as any)) {
components.push([node.tag, renderTag])
}
for (const child of (node.children || [])) {
components.push(...loadComponents(child, tags))
}
return components
}
}
export default defineTransformer({
name: 'component-resolver',
extensions: ['.*'],
async transform (content, options = {}) {
if (process.server) {
// This transformer is only needed on client side to resolve components
return content
}

const _components = await resolveContentComponents(content.body, {
...(options?.tags || {}),
...(content._components || {})
})

content._components = _components
return content
}
})

0 comments on commit 3f6efe8

Please sign in to comment.