1 | const { join, resolve } = require('path')
|
2 | const fs = require('graceful-fs').promises
|
3 | const mkdirp = require('mkdirp')
|
4 | const defu = require('defu')
|
5 | const logger = require('consola').withScope('@nuxt/content')
|
6 | const hash = require('hasha')
|
7 |
|
8 | const middleware = require('./middleware')
|
9 | const Database = require('./database')
|
10 | const WS = require('./ws')
|
11 | const { getDefaults, processMarkdownOptions } = require('./utils')
|
12 |
|
13 | module.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 |
|
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 |
|
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 |
|
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 |
|
113 | await database.init()
|
114 |
|
115 |
|
116 | this.nuxt.hook('close', () => database.close())
|
117 |
|
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 |
|
141 | await this.nuxt.callHook('content:ready', $content)
|
142 |
|
143 |
|
144 | this.nuxt.hook('vue-renderer:context', (ssrContext) => {
|
145 | ssrContext.$content = $content
|
146 | })
|
147 |
|
148 |
|
149 | if (options.markdown.prism.theme && !options.markdown.highlighter) {
|
150 | this.nuxt.options.css.push(options.markdown.prism.theme)
|
151 | }
|
152 |
|
153 |
|
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 |
|
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 |
|
175 | if (isSSG) {
|
176 |
|
177 | const dbHash = hash(JSON.stringify(database.items._data)).substr(0, 8)
|
178 |
|
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 |
|
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 |
|
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
|
208 | let routerBasePath = this.options.router.base
|
209 |
|
210 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
277 |
|
278 | new URL(string)
|
279 | } catch (_) {
|
280 | return false
|
281 | }
|
282 | return true
|
283 | }
|
284 | }
|
285 |
|
286 | module.exports.Database = Database
|
287 | module.exports.middleware = middleware
|
288 | module.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 | }
|