Skip to content

Commit 1774a0b

Browse files
authored
refactor(storage): useContentStorage composable (#175)
1 parent 1513957 commit 1774a0b

File tree

5 files changed

+109
-103
lines changed

5 files changed

+109
-103
lines changed

src/runtime/components/ContentPreviewMode.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,14 @@ const closePreviewMode = async () => {
4747
}
4848
4949
const sync = async (data: PreviewResponse) => {
50-
const isUpdated = await props.syncPreview(data)
50+
const storageReady = await props.syncPreview(data)
5151
5252
if (previewReady.value === true) {
5353
// Preview already ready, no need to sync again
5454
return
5555
}
5656
57-
// If data is not updated, it means the storage is not ready yet and we should try again
58-
if (!isUpdated) {
57+
if (!storageReady) {
5958
setTimeout(() => sync(data), 1000)
6059
return
6160
}
@@ -70,7 +69,6 @@ const sync = async (data: PreviewResponse) => {
7069
// Remove query params in url to refresh page (in case of 404 with no SPA fallback)
7170
await router.replace({ query: {} })
7271
73-
// @ts-expect-error custom hook
7472
nuxtApp.callHook('nuxt-studio:preview:ready')
7573
7674
if (window.parent && window.self !== window.parent) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { ParsedContent } from '@nuxt/content/types'
2+
import type { PreviewFile } from '../types'
3+
import { useNuxtApp, useState, queryContent } from '#imports'
4+
5+
export const useContentStorage = () => {
6+
const nuxtApp = useNuxtApp()
7+
const contentPathMap = {} as Record<string, ParsedContent>
8+
const storage = useState<Storage | null>('studio-client-db', () => null)
9+
10+
// Initialize storage
11+
if (!storage.value) {
12+
nuxtApp.hook('content:storage', (_storage: Storage) => {
13+
storage.value = _storage
14+
})
15+
16+
// Call `queryContent` to trigger `content:storage` hook
17+
queryContent('/non-existing-path').findOne()
18+
}
19+
20+
const findContentItem = async (path: string): Promise<ParsedContent | null> => {
21+
const previewToken = window.sessionStorage.getItem('previewToken')
22+
if (!path) {
23+
return null
24+
}
25+
path = path.replace(/\/$/, '')
26+
let content = await storage.value?.getItem(`${previewToken}:${path}`)
27+
if (!content) {
28+
content = await storage.value?.getItem(`cached:${path}`)
29+
}
30+
if (!content) {
31+
content = content = await storage.value?.getItem(path)
32+
}
33+
34+
// try finding content from contentPathMap
35+
if (!content) {
36+
content = contentPathMap[path || '/']
37+
}
38+
39+
return content as ParsedContent
40+
}
41+
42+
const updateContentItem = (previewToken: string, file: PreviewFile) => {
43+
if (!storage.value) return
44+
45+
contentPathMap[file.parsed!._path!] = file.parsed!
46+
storage.value.setItem(`${previewToken}:${file.parsed?._id}`, JSON.stringify(file.parsed))
47+
}
48+
49+
const removeContentItem = async (previewToken: string, path: string) => {
50+
const content = await findContentItem(path)
51+
await storage.value?.removeItem(`${previewToken}:${path}`)
52+
53+
if (content) {
54+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
55+
delete contentPathMap[content._path!]
56+
const nonDraftContent = await findContentItem(content._id)
57+
if (nonDraftContent) {
58+
contentPathMap[nonDraftContent._path!] = nonDraftContent
59+
}
60+
}
61+
}
62+
63+
const removeAllContentItems = async (previewToken: string) => {
64+
const keys: string[] = await storage.value.getKeys(`${previewToken}:`)
65+
await Promise.all(keys.map(key => storage.value.removeItem(key)))
66+
}
67+
68+
const setPreviewMetaItems = async (previewToken: string, files: PreviewFile[]) => {
69+
const sources = new Set<string>(files.map(file => file.parsed!._id.split(':').shift()!))
70+
await storage.value.setItem(`${previewToken}$`, JSON.stringify({ ignoreSources: Array.from(sources) }))
71+
}
72+
73+
return {
74+
storage,
75+
findContentItem,
76+
updateContentItem,
77+
removeContentItem,
78+
removeAllContentItems,
79+
setPreviewMetaItems,
80+
}
81+
}

src/runtime/composables/useStudio.ts

Lines changed: 17 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,40 @@
11
import { createApp } from 'vue'
2-
import type { Storage } from 'unstorage'
32
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
4-
import { createDefu } from 'defu'
53
import type { RouteLocationNormalized } from 'vue-router'
64
import type { AppConfig } from 'nuxt/schema'
75
import ContentPreviewMode from '../components/ContentPreviewMode.vue'
8-
import { createSingleton, deepAssign, deepDelete, mergeDraft, StudioConfigFiles } from '../utils'
6+
import { createSingleton, deepAssign, deepDelete, defu, mergeDraft, StudioConfigFiles } from '../utils'
97
import type { PreviewFile, PreviewResponse, FileChangeMessagePayload } from '../types'
8+
import { useContentStorage } from './useContentStorage'
109
import { callWithNuxt } from '#app'
11-
import { useAppConfig, useNuxtApp, useRuntimeConfig, useState, useContentState, queryContent, ref, toRaw, useRoute, useRouter } from '#imports'
10+
import { useAppConfig, useNuxtApp, useRuntimeConfig, useContentState, ref, toRaw, useRoute, useRouter } from '#imports'
1211

1312
const useDefaultAppConfig = createSingleton(() => JSON.parse(JSON.stringify((useAppConfig()))))
1413

15-
const defu = createDefu((obj, key, value) => {
16-
if (Array.isArray(obj[key]) && Array.isArray(value)) {
17-
obj[key] = value
18-
return true
19-
}
20-
})
21-
2214
let dbFiles: PreviewFile[] = []
2315

2416
export const useStudio = () => {
2517
const nuxtApp = useNuxtApp()
18+
const { storage, findContentItem, updateContentItem, removeContentItem, removeAllContentItems, setPreviewMetaItems } = useContentStorage()
2619
const { studio: studioConfig, content: contentConfig } = useRuntimeConfig().public
27-
const contentPathMap = {} as Record<string, ParsedContent>
2820
const apiURL = window.sessionStorage.getItem('previewAPI') || studioConfig?.apiURL
2921

3022
// App config (required)
3123
const initialAppConfig = useDefaultAppConfig()
32-
const storage = useState<Storage | null>('studio-client-db', () => null)
33-
34-
if (!storage.value) {
35-
nuxtApp.hook('content:storage', (_storage: Storage) => {
36-
storage.value = _storage
37-
})
3824

39-
// Call `queryContent` to trigger `content:storage` hook
40-
queryContent('/non-existing-path').findOne()
41-
}
25+
const syncPreviewFiles = async (files: PreviewFile[]) => {
26+
const previewToken = window.sessionStorage.getItem('previewToken') as string
4227

43-
const syncPreviewFiles = async (contentStorage: Storage, files: PreviewFile[]) => {
44-
const previewToken = window.sessionStorage.getItem('previewToken')
4528
// Remove previous preview data
46-
const keys: string[] = await contentStorage.getKeys(`${previewToken}:`)
47-
await Promise.all(keys.map(key => contentStorage.removeItem(key)))
29+
removeAllContentItems(previewToken)
4830

4931
// Set preview meta
50-
const sources = new Set<string>(files.map(file => file.parsed!._id.split(':').shift()!))
51-
await contentStorage.setItem(`${previewToken}$`, JSON.stringify({ ignoreSources: Array.from(sources) }))
32+
setPreviewMetaItems(previewToken, files)
5233

5334
// Handle content files
5435
await Promise.all(
55-
files.map((item) => {
56-
contentPathMap[item.parsed!._path!] = item.parsed!
57-
return contentStorage.setItem(`${previewToken}:${item.parsed!._id}`, JSON.stringify(item.parsed))
36+
files.map((file) => {
37+
updateContentItem(previewToken, file)
5838
}),
5939
)
6040
}
@@ -94,7 +74,7 @@ export const useStudio = () => {
9474

9575
// Handle content files
9676
const contentFiles = mergedFiles.filter(item => !([StudioConfigFiles.appConfig, StudioConfigFiles.nuxtConfig].includes(item.path)))
97-
await syncPreviewFiles(storage.value, contentFiles)
77+
await syncPreviewFiles(contentFiles)
9878

9979
const appConfig = mergedFiles.find(item => item.path === StudioConfigFiles.appConfig)
10080
syncPreviewAppConfig(appConfig?.parsed as ParsedContent)
@@ -130,58 +110,12 @@ export const useStudio = () => {
130110
}).mount(el)
131111
}
132112

133-
// Content Helpers
134-
const findContentWithId = async (path: string): Promise<ParsedContent | null> => {
135-
const previewToken = window.sessionStorage.getItem('previewToken')
136-
if (!path) {
137-
return null
138-
}
139-
path = path.replace(/\/$/, '')
140-
let content = await storage.value?.getItem(`${previewToken}:${path}`)
141-
if (!content) {
142-
content = await storage.value?.getItem(`cached:${path}`)
143-
}
144-
if (!content) {
145-
content = content = await storage.value?.getItem(path)
146-
}
147-
148-
// try finding content from contentPathMap
149-
if (!content) {
150-
content = contentPathMap[path || '/']
151-
}
152-
153-
return content as ParsedContent
154-
}
155-
156-
const updateContent = (content: PreviewFile) => {
157-
const previewToken = window.sessionStorage.getItem('previewToken')
158-
if (!storage.value) return
159-
160-
contentPathMap[content.parsed!._path!] = content.parsed!
161-
storage.value.setItem(`${previewToken}:${content.parsed?._id}`, JSON.stringify(content.parsed))
162-
}
163-
164-
const removeContentWithId = async (path: string) => {
165-
const previewToken = window.sessionStorage.getItem('previewToken')
166-
const content = await findContentWithId(path)
167-
await storage.value?.removeItem(`${previewToken}:${path}`)
168-
169-
if (content) {
170-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
171-
delete contentPathMap[content._path!]
172-
const nonDraftContent = await findContentWithId(content._id)
173-
if (nonDraftContent) {
174-
contentPathMap[nonDraftContent._path!] = nonDraftContent
175-
}
176-
}
177-
}
178-
179113
const requestRerender = async () => {
180114
if (contentConfig?.documentDriven) {
181115
const { pages } = callWithNuxt(nuxtApp, useContentState)
182116

183117
const contents = await Promise.all(Object.keys(pages.value).map(async (key) => {
184-
return await findContentWithId(pages.value[key]?._id ?? key)
118+
return await findContentItem(pages.value[key]?._id ?? key)
185119
}))
186120

187121
pages.value = contents.reduce((acc, item, index) => {
@@ -196,19 +130,6 @@ export const useStudio = () => {
196130
}
197131

198132
return {
199-
apiURL,
200-
contentStorage: storage,
201-
202-
syncPreviewFiles,
203-
syncPreviewAppConfig,
204-
requestPreviewSynchronization,
205-
206-
findContentWithId,
207-
updateContent,
208-
removeContentWithId,
209-
210-
requestRerender,
211-
212133
mountPreviewUI,
213134
initiateIframeCommunication,
214135
}
@@ -258,7 +179,7 @@ export const useStudio = () => {
258179

259180
switch (type) {
260181
case 'nuxt-studio:editor:file-selected': {
261-
const content = await findContentWithId(payload.path)
182+
const content = await findContentItem(payload.path)
262183
if (!content) {
263184
// Do not navigate to another page if content is not found
264185
// This makes sure that user stays on the same page when navigation through directories in the editor
@@ -273,21 +194,19 @@ export const useStudio = () => {
273194
}
274195
break
275196
}
197+
case 'nuxt-studio:editor:media-changed':
276198
case 'nuxt-studio:editor:file-changed': {
199+
const previewToken = window.sessionStorage.getItem('previewToken') as string
277200
const { additions = [], deletions = [] } = payload as FileChangeMessagePayload
278201
for (const addition of additions) {
279-
await updateContent(addition)
202+
await updateContentItem(previewToken, addition)
280203
}
281204
for (const deletion of deletions) {
282-
await removeContentWithId(deletion.path)
205+
await removeContentItem(previewToken, deletion.path)
283206
}
284207
requestRerender()
285208
break
286209
}
287-
case 'nuxt-studio:preview:sync': {
288-
syncPreview(payload)
289-
break
290-
}
291210
case 'nuxt-studio:config:file-changed': {
292211
const { additions = [], deletions = [] } = payload as FileChangeMessagePayload
293212

src/runtime/plugins/preview.client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export default defineNuxtPlugin((nuxtApp) => {
3333

3434
// Listen to `content:storage` hook to get storage instance
3535
// There is some cases that `content:storage` hook is called before initializing preview
36-
// @ts-expect-error custom hook
3736
nuxtApp.hook('content:storage', (_storage: Storage) => {
3837
storage.value = _storage
3938
})

src/runtime/utils/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import { createDefu } from 'defu'
2+
13
export * from './files'
24

35
export const StudioConfigFiles = {
46
appConfig: 'app.config.ts',
57
nuxtConfig: 'nuxt.config.ts',
68
}
79

10+
export const defu = createDefu((obj, key, value) => {
11+
if (Array.isArray(obj[key]) && Array.isArray(value)) {
12+
obj[key] = value
13+
return true
14+
}
15+
})
16+
817
export const createSingleton = <T, Params extends Array<unknown>>(fn: () => T) => {
918
let instance: T | undefined
1019
return (_args?: Params) => {

0 commit comments

Comments
 (0)