UNPKG

8.63 kBJavaScriptView Raw
1const { join, resolve } = require('path')
2const fs = require('graceful-fs').promises
3const mkdirp = require('mkdirp')
4const defu = require('defu')
5const logger = require('consola').withScope('@nuxt/content')
6const hash = require('hasha')
7
8const middleware = require('./middleware')
9const Database = require('./database')
10const WS = require('./ws')
11const { getDefaults, processMarkdownOptions } = require('./utils')
12
13module.exports = async function (moduleOptions) {
14 const { nuxt } = this
15 const isSSG =
16 this.options.dev === false &&
17 (this.options.target === 'static' ||
18 this.options._generate ||
19 this.options.mode === 'spa')
20
21 const { content = {} } = Object.assign({}, this.options)
22 Object.assign(content, moduleOptions)
23
24 if (content.markdown && content.markdown.basePlugins) {
25 logger.warn(
26 'Using `markdown.basePlugins` is deprecated. Use `markdown.remarkPlugins` as a function instead.'
27 )
28 const basePlugins = [...content.markdown.basePlugins]
29 content.markdown.remarkPlugins = () => basePlugins
30 delete content.markdown.basePlugins
31 }
32 if (content.markdown && content.markdown.plugins) {
33 logger.warn(
34 'Using `markdown.plugins` is deprecated. Use `markdown.remarkPlugins` as an array instead.'
35 )
36 const plugins = [...content.markdown.plugins]
37 if (content.markdown.remarkPlugins) {
38 if (typeof content.markdown.remarkPlugins === 'function') {
39 const oldPlugins = [...content.markdown.remarkPlugins()]
40 content.markdown.remarkPlugins = () => oldPlugins.concat(plugins)
41 } else {
42 content.markdown.remarkPlugins = content.markdown.remarkPlugins.concat(
43 plugins
44 )
45 }
46 } else {
47 content.markdown.remarkPlugins = plugins
48 }
49 delete content.markdown.plugins
50 }
51
52 const defaults = getDefaults({ dev: this.options.dev })
53
54 const options = defu.arrayFn(content, defaults)
55 const relativeDir = options.dir
56 options.dir = resolve(this.options.srcDir, options.dir)
57
58 processMarkdownOptions(options, this.nuxt.resolver.resolvePath)
59
60 options.apiPrefixWithBase = options.apiPrefix
61 if (this.options.router.base) {
62 let baseRouter = this.options.router.base
63
64 if (baseRouter[0] === '/') {
65 baseRouter = baseRouter.substring(1)
66 }
67
68 options.apiPrefixWithBase = baseRouter + options.apiPrefix
69 }
70
71 nuxt.callHook('content:options', options)
72
73 // Nuxt hooks
74 const globalComponents = resolve(this.options.srcDir, 'components/global')
75 const dirStat = await fs.stat(globalComponents).catch(() => null)
76 if (dirStat && dirStat.isDirectory()) {
77 nuxt.hook('components:dirs', (dirs) => {
78 dirs.push({
79 path: '~/components/global',
80 global: true,
81 pathPrefix: false
82 })
83 })
84 } else {
85 // restart Nuxt on first component creation inside the dir
86 nuxt.options.watch.push(globalComponents)
87 }
88
89 this.nuxt.hook('generate:cache:ignore', ignore => ignore.push(relativeDir))
90
91 const ws = new WS({
92 apiPrefix: options.apiPrefixWithBase
93 })
94 this.nuxt.hook('listen', (server) => {
95 server.on('upgrade', (...args) => ws.callHook('upgrade', ...args))
96 })
97
98 const database = new Database({
99 ...options,
100 cwd: this.options.srcDir
101 })
102
103 // Database hooks
104 database.hook('file:beforeInsert', item =>
105 this.nuxt.callHook('content:file:beforeInsert', item, database)
106 )
107 database.hook('file:beforeParse', file =>
108 this.nuxt.callHook('content:file:beforeParse', file)
109 )
110 database.hook('file:updated', event => ws.broadcast(event))
111
112 // Initialize database from file system
113 await database.init()
114
115 // close database when Nuxt closes
116 this.nuxt.hook('close', () => database.close())
117 // listen to nuxt server to updrag
118
119 const $content = function () {
120 let options
121 const paths = []
122 Array.from(arguments).forEach((argument) => {
123 if (typeof argument === 'string') {
124 paths.push(argument)
125 } else if (typeof argument === 'object') {
126 options = argument
127 }
128 })
129
130 const path = paths
131 .join('/')
132 .replace(/\/+/g, '/')
133 .replace(/^\//, '')
134
135 return database.query(`/${path}`, options)
136 }
137 $content.database = database
138 module.exports.$content = $content
139
140 // Call content:ready hook
141 await this.nuxt.callHook('content:ready', $content)
142
143 // Add $content reference to ssrContext
144 this.nuxt.hook('vue-renderer:context', (ssrContext) => {
145 ssrContext.$content = $content
146 })
147
148 // Add prism theme
149 if (options.markdown.prism.theme && !options.markdown.highlighter) {
150 this.nuxt.options.css.push(options.markdown.prism.theme)
151 }
152
153 // Add content server middleware
154 this.addServerMiddleware({
155 path: options.apiPrefix,
156 handler: middleware({
157 ws,
158 database,
159 dir: options.dir,
160 watch: options.watch
161 })
162 })
163
164 // Add server plugin
165 this.addPlugin({
166 fileName: 'content/plugin.server.js',
167 src: join(__dirname, '../templates/plugin.server.js'),
168 options: {
169 watch: options.watch,
170 liveEdit: options.liveEdit
171 }
172 })
173
174 /* istanbul ignore if */
175 if (isSSG) {
176 // Create a hash to fetch the database
177 const dbHash = hash(JSON.stringify(database.items._data)).substr(0, 8)
178 // Pass the hash to the publicRuntimeConfig to be used in client side
179 if (this.options.publicRuntimeConfig) {
180 this.options.publicRuntimeConfig.content = { dbHash }
181 } else {
182 this.nuxt.hook('vue-renderer:ssr:context', (renderContext) => {
183 renderContext.nuxt.content = { dbHash }
184 })
185 }
186 // Write db.json
187 this.nuxt.hook('generate:distRemoved', async () => {
188 const dir = resolve(this.options.buildDir, 'dist', 'client', 'content')
189
190 await mkdirp(dir)
191 await fs.writeFile(
192 join(dir, `db-${dbHash}.json`),
193 database.db.serialize(),
194 'utf-8'
195 )
196 })
197
198 // Add client plugin
199 this.addTemplate({
200 fileName: 'content/plugin.client.lazy.js',
201 src: join(__dirname, '../templates/plugin.static.lazy.js'),
202 options: {
203 fullTextSearchFields: options.fullTextSearchFields,
204 dirs: database.dirs
205 }
206 })
207 let publicPath = this.options.build.publicPath // can be an url
208 let routerBasePath = this.options.router.base
209
210 /* istanbul ignore if */
211 if (publicPath[publicPath.length - 1] !== '/') {
212 publicPath += '/'
213 }
214 if (routerBasePath[routerBasePath.length - 1] === '/') {
215 routerBasePath = routerBasePath.slice(0, -1)
216 }
217
218 this.addPlugin({
219 fileName: 'content/plugin.client.js',
220 src: join(__dirname, '../templates/plugin.static.js'),
221 options: {
222 // if publicPath is an URL, use public path, if not, add basepath before it
223 dbPath: isUrl(publicPath)
224 ? `${publicPath}content`
225 : `${routerBasePath}${publicPath}content`
226 }
227 })
228 } else {
229 this.addPlugin({
230 fileName: 'content/plugin.client.js',
231 src: join(__dirname, '../templates/plugin.client.js'),
232 options: {
233 apiPrefix: options.apiPrefixWithBase,
234 watch: options.watch,
235 liveEdit: options.liveEdit,
236 readyCallbackName: this.options.globals.readyCallback(
237 this.options.globalName
238 )
239 }
240 })
241 }
242
243 // Add client plugin QueryBuilder
244 this.addTemplate({
245 fileName: 'content/query-builder.js',
246 src: join(
247 __dirname,
248 isSSG ? 'query-builder.js' : '../templates/query-builder.js'
249 )
250 })
251
252 // Add client plugin component
253 this.addTemplate({
254 fileName: 'content/nuxt-content.js',
255 src: join(__dirname, '../templates/nuxt-content.js')
256 })
257 if (options.watch && options.liveEdit) {
258 // Add dev client plugin component
259 this.addTemplate({
260 fileName: 'content/nuxt-content.dev.vue',
261 src: join(__dirname, '../templates/nuxt-content.dev.vue'),
262 options: {
263 apiPrefix: options.apiPrefixWithBase,
264 editor: options.editor
265 }
266 })
267 // Add dev editor component
268 this.addTemplate({
269 fileName: 'content/editor.vue',
270 src: join(__dirname, '..', 'templates', 'editor.vue')
271 })
272 }
273
274 function isUrl (string) {
275 try {
276 // quick test if the string is an URL
277 // eslint-disable-next-line no-new
278 new URL(string)
279 } catch (_) {
280 return false
281 }
282 return true
283 }
284}
285
286module.exports.Database = Database
287module.exports.middleware = middleware
288module.exports.getOptions = (userOptions = {}) => {
289 const defaults = getDefaults({ dev: process.env.NODE_ENV !== 'production' })
290 const options = defu.arrayFn(userOptions, defaults)
291
292 processMarkdownOptions(options)
293
294 return options
295}