@nuxtjs/prismic
Version:
Easily connect your Nuxt application to your content hosted on Prismic
513 lines (464 loc) • 12.2 kB
text/typescript
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
}