Skip to content

Commit 9d7f3a0

Browse files
authored
feat(content): support useCache option (#772)
1 parent d4dce1e commit 9d7f3a0

File tree

6 files changed

+143
-17
lines changed

6 files changed

+143
-17
lines changed

docs/content/en/configuration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,13 @@ Your component should implement the following:
524524

525525
You should be aware that you get the full markdown file content so this includes the front-matter. You can use `gray-matter` to split and join the markdown and the front-matter.
526526

527+
### `useCache`
528+
529+
- Type: `Boolean`
530+
- Default: `false`
531+
532+
When `true`, the production server (`nuxt start`) will use cached version of the content (generated after running `nuxt build`) instead of parsing files. This improves app startup time, but makes app unaware of any content changes.
533+
527534
## Defaults
528535

529536
```js{}[nuxt.config.js]
@@ -535,6 +542,7 @@ export default {
535542
fullTextSearchFields: ['title', 'description', 'slug', 'text'],
536543
nestedProperties: [],
537544
liveEdit: true,
545+
useCache: false,
538546
markdown: {
539547
remarkPlugins: [
540548
'remark-squeeze-paragraphs',

packages/content/lib/database.js

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { join, extname } = require('path')
22
const fs = require('graceful-fs').promises
3+
const mkdirp = require('mkdirp')
34
const Hookable = require('hookable')
45
const chokidar = require('chokidar')
56
const JSON5 = require('json5')
@@ -18,18 +19,21 @@ class Database extends Hookable {
1819
constructor (options) {
1920
super()
2021
this.dir = options.dir || process.cwd()
21-
this.cwd = options.cwd || process.cwd()
22+
this.srcDir = options.srcDir || process.cwd()
23+
this.buildDir = options.buildDir || process.cwd()
24+
this.useCache = options.useCache || false
2225
this.markdown = new Markdown(options.markdown)
2326
this.yaml = new YAML(options.yaml)
2427
this.csv = new CSV(options.csv)
2528
this.xml = new XML(options.xml)
2629
// Create Loki database
2730
this.db = new Loki('content.db')
2831
// Init collection
29-
this.items = this.db.addCollection('items', {
32+
this.itemsCollectionOptions = {
3033
fullTextSearch: options.fullTextSearchFields.map(field => ({ field })),
3134
nestedProperties: options.nestedProperties
32-
})
35+
}
36+
this.items = this.db.addCollection('items', this.itemsCollectionOptions)
3337
// User Parsers
3438
this.extendParser = options.extendParser || {}
3539
this.extendParserExtensions = Object.keys(this.extendParser)
@@ -58,19 +62,65 @@ class Database extends Hookable {
5862
}, this.options)
5963
}
6064

65+
async init () {
66+
if (this.useCache) {
67+
try {
68+
return await this.initFromCache()
69+
} catch (error) {}
70+
}
71+
72+
await this.initFromFilesystem()
73+
}
74+
6175
/**
6276
* Clear items in database and load files into collection
6377
*/
64-
async init () {
78+
async initFromFilesystem () {
79+
const startTime = process.hrtime()
6580
this.dirs = ['/']
6681
this.items.clear()
67-
68-
const startTime = process.hrtime()
6982
await this.walk(this.dir)
7083
const [s, ns] = process.hrtime(startTime)
7184
logger.info(`Parsed ${this.items.count()} files in ${s}.${Math.round(ns / 1e8)} seconds`)
7285
}
7386

87+
async initFromCache () {
88+
const startTime = process.hrtime()
89+
const cacheFilePath = join(this.buildDir, this.db.filename)
90+
const cacheFileData = await fs.readFile(cacheFilePath, 'utf-8')
91+
const cacheFileJson = JSON.parse(cacheFileData)
92+
93+
this.db.loadJSONObject(cacheFileJson)
94+
95+
// recreate references
96+
this.items = this.db.getCollection('items')
97+
this.dirs = this.items.mapReduce(doc => doc.dir, dirs => [...new Set(dirs)])
98+
99+
const [s, ns] = process.hrtime(startTime)
100+
logger.info(`Loaded ${this.items.count()} documents from cache in ${s},${Math.round(ns / 1e8)} seconds`)
101+
}
102+
103+
/**
104+
* Store database info file
105+
* @param {string} [dir] - Directory containing database dump file.
106+
* @param {string} [filename] - Database dump filename.
107+
*/
108+
async save (dir, filename) {
109+
dir = dir || this.buildDir
110+
filename = filename || this.db.filename
111+
112+
await mkdirp(dir)
113+
await fs.writeFile(join(dir, filename), this.db.serialize(), 'utf-8')
114+
}
115+
116+
async rebuildCache () {
117+
logger.info('Rebuilding content cache')
118+
this.db = new Loki('content.db')
119+
this.items = this.db.addCollection('items', this.itemsCollectionOptions)
120+
await this.initFromFilesystem()
121+
await this.save()
122+
}
123+
74124
/**
75125
* Walk dir tree recursively
76126
* @param {string} dir - Directory to browse.
@@ -145,7 +195,7 @@ class Database extends Hookable {
145195

146196
const document = this.items.findOne({ path: item.path })
147197

148-
logger.info(`Updated ${path.replace(this.cwd, '.')}`)
198+
logger.info(`Updated ${path.replace(this.srcDir, '.')}`)
149199
if (document) {
150200
this.items.update({ $loki: document.$loki, meta: document.meta, ...item })
151201
return
@@ -171,7 +221,7 @@ class Database extends Hookable {
171221
*/
172222
async parseFile (path) {
173223
const extension = extname(path)
174-
// If unkown extension, skip
224+
// If unknown extension, skip
175225
if (!EXTENSIONS.includes(extension) && !this.extendParserExtensions.includes(extension)) {
176226
return
177227
}
@@ -204,7 +254,7 @@ class Database extends Hookable {
204254
// Force data to be an array
205255
data = Array.isArray(data) ? data : [data]
206256
} catch (err) {
207-
logger.warn(`Could not parse ${path.replace(this.cwd, '.')}:`, err.message)
257+
logger.warn(`Could not parse ${path.replace(this.srcDir, '.')}:`, err.message)
208258
return null
209259
}
210260

packages/content/lib/index.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const { join, resolve } = require('path')
22
const fs = require('graceful-fs').promises
3-
const mkdirp = require('mkdirp')
43
const defu = require('defu')
54
const logger = require('consola').withScope('@nuxt/content')
65
const hash = require('hasha')
@@ -95,11 +94,21 @@ module.exports = async function (moduleOptions) {
9594
server.on('upgrade', (...args) => ws.callHook('upgrade', ...args))
9695
})
9796

97+
const useCache = options.useCache && !this.options.dev && this.options.ssr
98+
9899
const database = new Database({
99100
...options,
100-
cwd: this.options.srcDir
101+
srcDir: this.options.srcDir,
102+
buildDir: resolve(this.options.buildDir, 'content'),
103+
useCache
101104
})
102105

106+
if (useCache) {
107+
this.nuxt.hook('builder:prepared', async () => {
108+
await database.rebuildCache()
109+
})
110+
}
111+
103112
// Database hooks
104113
database.hook('file:beforeInsert', item =>
105114
this.nuxt.callHook('content:file:beforeInsert', item, database)
@@ -187,12 +196,7 @@ module.exports = async function (moduleOptions) {
187196
this.nuxt.hook('generate:distRemoved', async () => {
188197
const dir = resolve(this.options.buildDir, 'dist', 'client', 'content')
189198

190-
await mkdirp(dir)
191-
await fs.writeFile(
192-
join(dir, `db-${dbHash}.json`),
193-
database.db.serialize(),
194-
'utf-8'
195-
)
199+
await database.save(dir, `db-${dbHash}.json`)
196200
})
197201

198202
// Add client plugin

packages/content/lib/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { camelCase } = require('change-case')
44
const getDefaults = ({ dev = false } = {}) => ({
55
editor: './editor.vue',
66
watch: dev,
7+
useCache: false,
78
liveEdit: true,
89
apiPrefix: '_content',
910
dir: 'content',

packages/content/test/cache.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const path = require('path')
2+
const fs = require('graceful-fs').promises
3+
const { build, init, generatePort, loadConfig } = require('@nuxtjs/module-test-utils')
4+
5+
describe('content cache', () => {
6+
const config = {
7+
...loadConfig(__dirname),
8+
buildDir: path.join(__dirname, 'fixture', '.nuxt-dev'),
9+
content: {
10+
useCache: true
11+
}
12+
}
13+
14+
const dbFilePath = path.join(config.buildDir, 'content', 'content.db')
15+
16+
describe('during build', () => {
17+
let nuxt
18+
19+
beforeAll(async () => {
20+
fs.unlink(dbFilePath).catch(() => {});
21+
({ nuxt } = (await build(config)))
22+
}, 60000)
23+
24+
afterAll(async () => {
25+
await nuxt.close()
26+
})
27+
28+
test('should be generated', async () => {
29+
await expect(fs.access(dbFilePath)).resolves.not.toThrow()
30+
})
31+
32+
test('should be a valid json', async () => {
33+
const fileTextContent = await fs.readFile(dbFilePath)
34+
await expect(() => JSON.parse(fileTextContent)).not.toThrow()
35+
})
36+
})
37+
38+
describe('during start in production mode', () => {
39+
const mockDbDump = '{"_env":"NODEJS","_serializationMethod":"normal","_autosave":false,"_autosaveInterval":5000,"_collections":[{"name":"items","unindexedSortComparator":"js","defaultLokiOperatorPackage":"js","_dynamicViews":[],"uniqueNames":[],"transforms":{},"rangedIndexes":{},"_data":[{"slug":"about","title":"Serialized test","toc":[],"body":{"type":"root","children":[{"type":"element","tag":"p","props":{},"children":[{"type":"text","value":"This is the serialized page!"}]}]},"text":"\\nThis is the serialized page!\\n","dir":"/","path":"/about","extension":".md","createdAt":"2021-02-11T22:10:21.655Z","updatedAt":"2021-02-12T20:13:23.079Z","meta":{"version":0,"revision":0,"created":1613160831274},"$loki":1}],"idIndex":[1],"maxId":1,"_dirty":true,"_nestedProperties":[],"transactional":false,"asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"cloneObjects":false,"cloneMethod":"deep","changes":[],"_fullTextSearch":{"ii":{"title":{"_store":true,"_optimizeChanges":true,"docCount":1,"docStore":[[0,{"fieldLength":2}]],"totalFieldLength":2,"root":{"k":[115,116],"v":[{"k":[101],"v":[{"k":[114],"v":[{"k":[105],"v":[{"k":[97],"v":[{"k":[108],"v":[{"k":[105],"v":[{"k":[122],"v":[{"k":[101],"v":[{"k":[100],"v":[{"d":{"df":1,"dc":[[0,1]]}}]}]}]}]}]}]}]}]}]},{"k":[101],"v":[{"k":[115],"v":[{"k":[116],"v":[{"d":{"df":1,"dc":[[0,1]]}}]}]}]}]}},"description":{"_store":true,"_optimizeChanges":true,"docCount":0,"docStore":[],"totalFieldLength":0,"root":{}},"slug":{"_store":true,"_optimizeChanges":true,"docCount":1,"docStore":[[0,{"fieldLength":1}]],"totalFieldLength":1,"root":{"k":[97],"v":[{"k":[98],"v":[{"k":[111],"v":[{"k":[117],"v":[{"k":[116],"v":[{"d":{"df":1,"dc":[[0,1]]}}]}]}]}]}]}},"text":{"_store":true,"_optimizeChanges":true,"docCount":1,"docStore":[[0,{"fieldLength":5}]],"totalFieldLength":5,"root":{"k":[116,105,115,112],"v":[{"k":[104],"v":[{"k":[105,101],"v":[{"k":[115],"v":[{"d":{"df":1,"dc":[[0,1]]}}]},{"d":{"df":1,"dc":[[0,1]]}}]}]},{"k":[115],"v":[{"d":{"df":1,"dc":[[0,1]]}}]},{"k":[101],"v":[{"k":[114],"v":[{"k":[105],"v":[{"k":[97],"v":[{"k":[108],"v":[{"k":[105],"v":[{"k":[122],"v":[{"k":[101],"v":[{"k":[100],"v":[{"d":{"df":1,"dc":[[0,1]]}}]}]}]}]}]}]}]}]}]},{"k":[97],"v":[{"k":[103],"v":[{"k":[101],"v":[{"k":[33],"v":[{"d":{"df":1,"dc":[[0,1]]}}]}]}]}]}]}}}}}],"databaseVersion":1.5,"engineVersion":1.5,"filename":"content.db","_persistenceAdapter":null,"_persistenceMethod":null,"_throttledSaves":true}'
40+
let nuxt
41+
let $content
42+
43+
beforeAll(async () => {
44+
await fs.writeFile(dbFilePath, mockDbDump)
45+
nuxt = await init(config)
46+
await nuxt.listen(await generatePort())
47+
$content = require('@nuxt/content').$content
48+
}, 60000)
49+
50+
afterAll(async () => {
51+
await nuxt.close()
52+
})
53+
54+
test('should use cached db', async () => {
55+
const item = await $content('about').fetch()
56+
57+
expect(item).toEqual(expect.objectContaining({
58+
title: 'Serialized test'
59+
}))
60+
})
61+
})
62+
})

packages/content/test/options.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('options', () => {
3232
expect(options).toEqual(expect.objectContaining({
3333
apiPrefix: '_content',
3434
dir: 'content',
35+
useCache: false,
3536
fullTextSearchFields: ['title', 'description', 'slug', 'text'],
3637
nestedProperties: [],
3738
csv: {},

0 commit comments

Comments
 (0)