Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ is added automatically to requests. Defaults to `false`.
- `'legacy'`: Legacy format with props and slots flattened at the same level. Explicitly configure this for improved compatibility with older backends.
Defaults to `'explicit'`.

- `jsonRender`: Enables rendering custom_elements' [json-render](https://github.com/vercel-labs/json-render) output format — a flat element map (`{root, elements}`) instead of a nested custom element tree. Requires installing the optional dependencies: `npm install @json-render/vue zod`. When a page's content is a json-render spec, it renders via `@json-render/vue`; element types resolve through the same component resolution as the other formats, named slots and `drupal-markup` inline-HTML elements included. Defaults to `false`.

- `customErrorPages`: By default, error pages provided by Drupal (e.g. 403, 404 page) are shown,
while keeping the right status code. By enabling customErrorPages, the regular Nuxt error
pages are shown instead, such that the pages can be customized with Nuxt. Defaults to `false`.
Expand Down
57 changes: 54 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,20 @@
"nuxt-component-preview": "^1.1.1",
"ufo": "^1.6.4"
},
"peerDependencies": {
"@json-render/vue": ">=0.20.0",
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"@json-render/vue": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
"@json-render/vue": "^0.20.0",
"@nuxt/eslint": "^1.17.0",
"@nuxt/eslint-config": "^1.17.0",
"@nuxt/kit": "^4.5.2",
Expand All @@ -55,6 +68,7 @@
"playwright-core": "^1.62.1",
"typescript": "^5.9.3",
"vitest": "^4.1.11",
"vue": "^3.5.41"
"vue": "^3.5.41",
"zod": "^4.0.0"
}
}
19 changes: 18 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNuxtModule, addPlugin, addServerPlugin, createResolver, addImportsDir, addServerHandler, addImports, installModule } from '@nuxt/kit'
import { defineNuxtModule, addPlugin, addServerPlugin, createResolver, addImportsDir, addServerHandler, addImports, addComponent, installModule } from '@nuxt/kit'
import { defu } from 'defu'
import type { NuxtOptionsWithDrupalCe } from './runtime/types'

Expand Down Expand Up @@ -63,6 +63,11 @@ export interface ModuleOptions {
enableComponentPreview?: boolean
/** Extra Drupal JS URLs (substring match) the library loader must not load. */
skipLibraryScripts?: string[]
/**
* Render custom_elements' json-render output format. Requires the optional
* `@json-render/vue` dependency (plus its `zod` peer) to be installed.
*/
jsonRender?: boolean
}

export default defineNuxtModule<ModuleOptions>({
Expand Down Expand Up @@ -91,6 +96,7 @@ export default defineNuxtModule<ModuleOptions>({
disableFormHandler: false,
enableComponentPreview: true,
skipLibraryScripts: [],
jsonRender: false,
},
async setup(options, nuxt) {
const nuxtOptions = nuxt.options as NuxtOptionsWithDrupalCe
Expand All @@ -114,6 +120,17 @@ export default defineNuxtModule<ModuleOptions>({
addPlugin(resolve(runtimeDir, 'plugins/payloadPath.client'))
addPlugin(resolve(runtimeDir, 'plugins/drupalMarkup'))

// json-render support is opt-in: the component statically imports the
// optional @json-render/vue dependency, so it must stay unregistered (and
// unbundled) unless enabled.
if (options.jsonRender) {
addComponent({
name: 'DrupalCeJsonRender',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not make sense as component really. this is the wrong tool

filePath: resolve(runtimeDir, 'components/DrupalCeJsonRender'),
global: true,
})
}

// Add form handler middleware if not disabled (via boolean)
if (!(options.disableFormHandler === true)) {
addServerHandler({
Expand Down
79 changes: 79 additions & 0 deletions src/runtime/components/DrupalCeJsonRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { PropType, VNode } from 'vue'
import { defineComponent, computed, h } from 'vue'
import { JSONUIProvider, Renderer } from '@json-render/vue'
import { useDrupalCe } from '../composables/useDrupalCe'
import type { JsonRenderSpec, JsonRenderElement } from '../types'

type JsonRenderRegistry = Record<string, unknown>

/**
* Renders a custom_elements json-render spec via `@json-render/vue`.
*
* Registered globally only when the `jsonRender` module option is enabled, so
* the optional `@json-render/vue` dependency stays out of the bundle
* otherwise.
*
* - Element types resolve through the module's regular custom-element
* component resolution, so the same components serve markup, JSON and
* json-render rendering.
* - `drupal-markup` elements carry inline HTML in `props.markup` and render
* through the app's `drupal-markup` component (`content` prop).
* - json-render walks `children` only; the named `slots` of an element are
* bridged by rendering each slot entry as a sub-spec rooted at it.
*/
export default defineComponent({
name: 'DrupalCeJsonRender',
props: {
spec: {
type: Object as PropType<JsonRenderSpec>,
required: true,
},
},
setup(props) {
const { resolveCustomElement } = useDrupalCe()

const makeRegistryComponent = (component: unknown, type: string, spec: JsonRenderSpec, registry: JsonRenderRegistry) => {
const wrapper = (jsonRenderProps: { element: JsonRenderElement }, context: { slots: Record<string, (() => VNode[]) | undefined> }) => {
const element = jsonRenderProps.element
if (type === 'drupal-markup') {
return h(component as object, { content: element.props?.markup ?? '' })
}
const slotFunctions: Record<string, () => VNode | VNode[] | undefined> = {}
if (context.slots.default) {
slotFunctions.default = context.slots.default
}
Object.entries(element.slots ?? {}).forEach(([slotName, elementKeys]) => {
slotFunctions[slotName] = () => elementKeys.map(elementKey => h(Renderer, {
key: elementKey,
spec: { ...spec, root: elementKey },
registry,
}))
})
return h(component as object, element.props ?? {}, slotFunctions)
}
wrapper.props = ['element', 'emit', 'on', 'bindings', 'loading']
return wrapper
}

const registry = computed<JsonRenderRegistry>(() => {
const spec = props.spec
const registry: JsonRenderRegistry = {}
Object.values(spec.elements).forEach((element) => {
if (registry[element.type]) {
return
}
const component = resolveCustomElement(element.type)
if (component) {
registry[element.type] = makeRegistryComponent(component, element.type, spec, registry)
}
})
return registry
})

// The renderer requires its provider contexts; JSONUIProvider bundles
// them all with defaults.
return () => h(JSONUIProvider, { registry: registry.value }, {
default: () => h(Renderer, { spec: props.spec, registry: registry.value }),
})
},
})
25 changes: 24 additions & 1 deletion src/runtime/composables/useDrupalCe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import type { DrupalResolvedLibrary } from './drupalLibraryLoader'
import type { UseFetchOptions, AsyncData } from '#app'
import { callWithNuxt } from '#app'
import { useRuntimeConfig, useState, useFetch, navigateTo, createError, h, resolveComponent, setResponseStatus, useNuxtApp, useRequestHeaders, ref, watch, useRequestEvent, computed, useHead, toRef, useRoute, useRouter, useSlots } from '#imports'
import type { DrupalCePage, DrupalCeApiResponse } from '../../types'
import type { DrupalCePage, DrupalCeApiResponse, JsonRenderSpec } from '../../types'

/**
* Whether the given custom elements content is a json-render spec — the flat
* element-map format custom_elements can emit (issue #3580092) — rather than
* a nested explicit/legacy custom element object.
*/
export const isJsonRenderSpec = (content: unknown): content is JsonRenderSpec =>
typeof content === 'object' && content !== null && !Array.isArray(content)
&& typeof (content as JsonRenderSpec).root === 'string'
&& typeof (content as JsonRenderSpec).elements === 'object' && (content as JsonRenderSpec).elements !== null

// Cache the dynamic import of the library loader in a single module-level
// promise. All loadLibrary() callers await the *same* promise, so their
Expand Down Expand Up @@ -537,6 +547,19 @@ export const useDrupalCe = () => {
return customElements.map(element => renderCustomElementsToVNodes(element))
}

// Handle the json-render format: a flat element map plus a root reference.
if (isJsonRenderSpec(customElements)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not have to guess here. when the setting is on, use it.

// Resolved directly by name: the component only exists when the
// `jsonRender` module option registered it, and the custom-element
// fallback resolution must not kick in for this internal component.
const component = useNuxtApp().vueApp.component('DrupalCeJsonRender')
if (component) {
return h(component, { spec: customElements })
}
console.error('[nuxtjs-drupal-ce] Received a json-render spec, but json-render support is not enabled. Set the `jsonRender` module option and install the optional `@json-render/vue` dependency (plus its `zod` peer).')
return null
}

// Handle single custom element object based on configured format
if (config.customElementJsonFormat === 'explicit') {
// Verify format is explicit: check for keys that are NOT element/props/slots
Expand Down
24 changes: 24 additions & 0 deletions src/runtime/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,32 @@ export type CustomElementContent =
| undefined
| string
| CustomElementContentObject
| JsonRenderSpec
| Array<string | CustomElementContentObject>

/**
* One element of a json-render spec.
*
* `children` and slot entries reference other elements of the spec by key.
* A `drupal-markup` element carries inline HTML in `props.markup`.
*/
export interface JsonRenderElement {
type: string
props?: Record<string, any>
children?: string[]
slots?: Record<string, string[]>
}

/**
* json-render format custom elements content: a flat element map plus the key
* of the root element, as emitted by custom_elements' json-render output
* format (drupal.org/project/custom_elements, issue #3580092).
*/
export interface JsonRenderSpec {
root: string
elements: Record<string, JsonRenderElement>
}

/**
* Metatags structure
*/
Expand Down
Loading