UNPKG

@nuxtjs/prismic

Version:

Easily connect your Nuxt application to your content hosted on Prismic

513 lines (464 loc) 12.2 kB
import { existsSync } from "node:fs" import { readFile } from "node:fs/promises" import { join } from "node:path" import { addComponent, addImports, addPlugin, addTemplate, createResolver, defineNuxtModule, extendPages, getNuxtVersion, useLogger, } from "@nuxt/kit" import type { ClientConfig } from "@prismicio/client" import { defu } from "defu" import { addDependency } from "nypm" import { readPackage } from "pkg-types" import { name, version } from "../package.json" /** * Prismic Nuxt module options. * * @see {@link https://prismic.io/docs/nuxt} * @see {@link https://prismic.io/docs/technical-reference/nuxtjs-prismic} */ export type PrismicModuleOptions = { /** * The Prismic repository name or full Content API endpoint to init the * module's client instance used to fetch content from a Prismic repository * with. * * @example * * ```typescript * // With a repository name * createClient("my-repo") * * // With a full Prismic Content API endpoint * createClient("https://my-repo.cdn.prismic.io/api/v2") * ``` * * @see {@link https://prismic.io/docs/technical-reference/prismicio-client} */ endpoint?: string /** * The Prismic environment in use by Slice Machine configured through * environment variables. * * @defaultValue `endpoint` value. * * @internal */ environment?: string /** * Configuration options that determines how content will be queries from the * Prismic repository. * * @see {@link https://prismic.io/docs/technical-reference/prismicio-client} */ clientConfig?: ClientConfig /** * An optional path to a file exporting a Prismic client instance used to * fetch content from a Prismic repository to configure the module with. * * @remarks * When provided, it takes precedence over the `endpoint` and `clientConfig` * options. * * @see {@link https://prismic.io/docs/technical-reference/prismicio-client} */ client?: string /** * The path to a file exporting a default link resolver used to resolve links * when route resolvers cannot be used. * * @see {@link https://prismic.io/docs/routes} */ linkResolver?: string /** * Desired path of the preview page used by Prismic to enter preview session. * * @remarks * `false` can be used to disable the preview page. * * @defaultValue `"/preview"` */ preview?: string | false /** * Whether to inject Prismic toolbar script. * * @remarks * The toolbar script is required for previews to work. * * @defaultValue `true` */ toolbar?: boolean /** * Controls which auto-imports are added by the module. * * - `"all"` will add all imports. * - `["vue"]` will add `@nuxtjs/prismic` and `@prismicio/vue` imports. * - `["javascript"]` will add `@prismicio/client` imports. * - `["content"]` will add the `Content` type import. * - `false` will not add any import. * * @defaultValue `["vue"]` * * @experimental */ imports?: false | "all" | ("vue" | "javascript" | "content")[] /** Options used by Prismic Vue components. */ components?: { /** * The path to a file exporting default components or shorthand definitions * for rich text and table components. * * @see {@link https://prismic.io/docs/fields/rich-text} * @see {@link https://prismic.io/docs/fields/table} */ richTextComponents?: string } } /** * Prismic Nuxt module options. * * @see {@link https://prismic.io/docs/nuxt} * @see {@link https://prismic.io/docs/technical-reference/nuxtjs-prismic} */ export type ModuleOptions = PrismicModuleOptions declare module "@nuxt/schema" { interface PublicRuntimeConfig { /** The Prismic Nuxt module options. */ prismic: PrismicModuleOptions } } const logger = useLogger("nuxt:prismic") async function addPrismicClient() { try { const pkg = await readPackage() if ( !pkg.dependencies?.["@prismicio/client"] && !pkg.devDependencies?.["@prismicio/client"] ) { await addDependency("@prismicio/client") logger.info("Added `@prismicio/client` required peer dependency") } } catch { // noop } } export default defineNuxtModule<PrismicModuleOptions>({ meta: { name, version, configKey: "prismic", compatibility: { nuxt: ">=3.7.0" }, }, onInstall() { return addPrismicClient() }, onUpgrade(_options: unknown, _nuxt: unknown, previousVersion: string) { const previousMajor = parseInt(previousVersion.split(".")[0]!) if (previousMajor < 4) { return addPrismicClient() } }, defaults: (nuxt): Required<PrismicModuleOptions> => { const nuxt3flavor = getNuxtVersion(nuxt).startsWith("3") && !nuxt.options?.future?.compatibilityVersion if (nuxt3flavor) { return { endpoint: "", environment: "", clientConfig: {}, client: "~/app/prismic/client", linkResolver: "~/app/prismic/linkResolver", preview: "/preview", toolbar: true, imports: ["vue"], components: { richTextComponents: "~/app/prismic/richTextComponents ", }, } } return { endpoint: "", environment: "", client: "~/prismic/client", linkResolver: "~/prismic/linkResolver", clientConfig: {}, preview: "/preview", toolbar: true, imports: ["vue"], components: { richTextComponents: "~/prismic/richTextComponents", }, } }, setup(options, nuxt) { const resolver = createResolver(import.meta.url) const moduleOptions: PrismicModuleOptions = defu( nuxt.options.runtimeConfig.public?.prismic, options, ) exposeRuntimeConfig() transpileDependencies() const ok = proxyUserFiles() if (!ok) return addRuntimePlugins() addAutoImports() addPreviewRoute() extendESLintConfig() function exposeRuntimeConfig() { nuxt.options.runtimeConfig.public ||= {} as typeof nuxt.options.runtimeConfig.public nuxt.options.runtimeConfig.public.prismic = moduleOptions } function transpileDependencies() { nuxt.options.build.transpile.push( resolver.resolve("runtime"), "@nuxtjs/prismic", "@prismicio/vue", ) nuxt.options.vite.optimizeDeps ||= {} nuxt.options.vite.optimizeDeps.exclude ||= [] nuxt.options.vite.optimizeDeps.exclude.push("@prismicio/vue") } function proxyUserFiles() { const proxyUserFileWithUndefinedFallback = ( filename: string, path: string, ): boolean => { const resolvedFilename = `prismic/proxy/${filename}.ts` const resolvedPath = path .replace(/^(~~|@@)/, nuxt.options.rootDir) .replace(/^(~|@)/, nuxt.options.srcDir) const maybeUserFile = fileExists(resolvedPath, [ "js", "mjs", "ts", "vue", ]) if (maybeUserFile) { // If user file exists, proxy it with vfs logger.info( `Using user-defined \`${filename}\` at \`${maybeUserFile.replace(nuxt.options.srcDir, "~").replace(nuxt.options.rootDir, "~~").replace(/\\/g, "/")}\``, ) addTemplate({ filename: resolvedFilename, getContents: () => `export { default } from '${path}'`, }) return true } else { // Else provide `undefined` fallback addTemplate({ filename: resolvedFilename, getContents: () => "export default undefined", }) return false } } const proxiedUserClient = proxyUserFileWithUndefinedFallback( "client", moduleOptions.client!, ) if ( !moduleOptions.endpoint && !proxiedUserClient && !process.env.NUXT_PUBLIC_PRISMIC_ENDPOINT ) { logger.warn( `\`endpoint\` option is missing and \`${moduleOptions.client}\` was not found. At least one of them is required for the module to run. Disabling module...`, ) return false } proxyUserFileWithUndefinedFallback( "linkResolver", moduleOptions.linkResolver!, ) proxyUserFileWithUndefinedFallback( "richTextComponents", moduleOptions.components!.richTextComponents!, ) return true } function addRuntimePlugins() { addPlugin(resolver.resolve("runtime/plugin")) addPlugin(resolver.resolve("runtime/plugin.client")) } function addAutoImports() { if (!moduleOptions.imports) return if ( moduleOptions.imports === "all" || moduleOptions.imports.includes("vue") ) { ;[ "PrismicImage", "PrismicLink", "PrismicText", "PrismicRichText", "PrismicTable", "SliceZone", "SliceSimulator", ].forEach((entry) => { addComponent({ name: entry, export: entry, filePath: "@prismicio/vue", }) }) addImports( [ "usePrismic", "getSliceComponentProps", "defineSliceZoneComponents", "getRichTextComponentProps", "getTableComponentProps", ].map((entry) => ({ name: entry, as: entry, from: "@prismicio/vue", })), ) addImports({ name: "usePrismicPreview", as: "usePrismicPreview", from: resolver.resolve("runtime/usePrismicPreview"), }) } if ( moduleOptions.imports === "all" || moduleOptions.imports.includes("javascript") ) { addImports( [ "asDate", "asLink", "asLinkAttrs", "asText", "asHTML", "asImageSrc", "asImageWidthSrcSet", "asImagePixelDensitySrcSet", "isFilled", ].map((entry) => ({ name: entry, as: entry, from: "@prismicio/client", })), ) } if ( moduleOptions.imports === "all" || moduleOptions.imports.includes("content") ) { addImports({ name: "Content", from: "@prismicio/client", typeFrom: "@prismicio/client", type: true, }) } } function addPreviewRoute() { if (moduleOptions.preview) { const maybeUserPreviewPage = fileExists( join( nuxt.options.srcDir, nuxt.options.dir.pages, moduleOptions.preview, ), ["js", "ts", "vue"], ) if (maybeUserPreviewPage) { logger.info( `Using user-defined preview page at \`${maybeUserPreviewPage .replace(join(nuxt.options.srcDir), "~") .replace(nuxt.options.rootDir, "~~") .replace( /\\/g, "/", )}\`, available at \`${moduleOptions.preview}\``, ) } else { logger.info( `Using default preview page, available at \`${moduleOptions.preview}\``, ) extendPages((pages) => { pages.unshift({ name: "prismic-preview", path: moduleOptions.preview as string, // Checked before file: resolver.resolve("runtime/PrismicPreview.vue"), }) }) } if (!moduleOptions.toolbar) { logger.warn( "`toolbar` option is disabled but `preview` is enabled. Previews won't work unless you manually load the toolbar.", ) } } } function extendESLintConfig() { nuxt.hook( // @ts-expect-error 3rd party hook "eslint:config:addons", ( addons: { name: string getConfigs: () => Promise<{ configs: string[] }> }[], ) => { addons.push({ name: "@nuxtjs/prismic", async getConfigs() { const configPath = resolver.resolve( nuxt.options.rootDir, "slicemachine.config.json", ) const configs: string[] = [] try { if (existsSync(configPath)) { const config = JSON.parse(await readFile(configPath, "utf-8")) if ( config && "libraries" in config && Array.isArray(config.libraries) ) { configs.push( JSON.stringify({ files: config.libraries.map( (library: string) => `${library.replace("./", "")}/**/index.vue`, ), rules: { "vue/multi-word-component-names": "off", }, }), ) } } } catch { // noop } return { configs } }, }) }, ) } }, }) function fileExists(path?: string, extensions = ["js", "ts"]): string | null { if (!path) { return null } else if (existsSync(path)) { return path } const extension = extensions.find((extension) => existsSync(`${path}.${extension}`), ) return extension ? `${path}.${extension}` : null }