Skip to content

Commit

Permalink
feat: json data watcher
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed May 21, 2024
1 parent 4722a6e commit f6cebe9
Show file tree
Hide file tree
Showing 32 changed files with 394 additions and 99 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
],
'prefer-arrow-callback': 'off',
'react/display-name': 'off',
'tailwindcss/migration-from-tailwind-2': 0,
},
}
12 changes: 7 additions & 5 deletions markdown/index.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
[
{
"paths": [
"./sections/guide/what-this.md"
"what-this.md"
],
"slug": "guide",
"title": "开始"
},
{
"paths": [
"./sections/usage/markdown.md",
"./sections/usage/draw.md",
"./sections/usage/custom-component.md",
"./sections/usage/highlight.md"
"custom-component.md",
"draw.md",
"highlight.md",
"markdown.md"
],
"slug": "usage",
"title": "使用"
}
]
5 changes: 4 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { bootstarp } from './plugins/json-watcher.mjs'

bootstarp()

/** @type {import('next').NextConfig} */
const nextConfig = {
output: process.env.NODE_ENV === 'production' ? 'export' : 'standalone',
reactStrictMode: false,

webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
config.plugins.push(
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
"@radix-ui/react-tabs": "1.0.4",
"@types/katex": "0.16.7",
"camelcase-keys": "9.1.3",
"chokidar": "^3.6.0",
"clsx": "2.1.1",
"colorjs.io": "0.5.0",
"devtools-detector": "2.0.17",
"foxact": "0.2.33",
"framer-motion": "11.2.4",
"globby": "^14.0.1",
"immer": "10.1.1",
"jotai": "2.8.0",
"js-yaml": "4.1.0",
Expand Down
9 changes: 9 additions & 0 deletions plugins/interface.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type DocumentGraph = SingleDocumentTree[]

export interface SingleDocumentTree {
title: string
paths: (string | PathWithMeta)[]
slug: string
}
interface PathMeta {}
export type PathWithMeta = [string, PathMeta?]
234 changes: 234 additions & 0 deletions plugins/json-watcher.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { existsSync, writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import { watch } from 'chokidar'
import { globbySync } from 'globby'

const __dirname = path.resolve(fileURLToPath(import.meta.url), '..')
const dataJsonPath = path.resolve(__dirname, '../markdown/index.json')
const workdirPath = path.resolve(__dirname, '../markdown/sections')
const pathGlob = workdirPath + '/**/*.md'

const readFsDataJsonData = async () => {
const hasFile = existsSync(dataJsonPath)

if (!hasFile) {
createDefaultDataJson()
return readFsDataJsonData()
}
const data = await readFile(dataJsonPath, 'utf8')

try {
return JSON.parse(data)
} catch {
console.error('JSON parser error.')
return []
}
}

/**
*
* @param {import('./interface').DocumentGraph} data
* @returns {Record<string, import('./interface').SingleDocumentTree & {pathSet: Set<string>}}
*/
const parseFsData = (data) => {
if (!Array.isArray(data)) throw new TypeError('exist data json is broken.')

const parsedMap = {}

data.forEach((item) => {
parsedMap[item.slug] = { ...item }

const pathSet = new Set()
parsedMap[item.slug].pathSet = pathSet

item.paths.forEach((path) => {
pathSet.add(Array.isArray(path) ? path[0] : path)
})
})

return parsedMap
}

export async function bootstarp() {
const patch = debounce(async () => {
const fsJsonData = await readFsDataJsonData()

const diffData = compareFsTreeWithExistJsonData(parseFsData(fsJsonData))
console.log('diff', diffData)
writeFileSync(
dataJsonPath,
JSON.stringify(patchDataJson(diffData, fsJsonData), null, 2),
'utf8',
)
}, 800)
await patch()
const watcher = watch(pathGlob)

watcher.on('add', (path) => {
patch()
})
watcher.on('unlink', (path) => {
patch()
})
}

/**
*
* @param {{add: Record<string,string[]>,remove:Record<string,string[]>}} diffData
* @param {import('./interface').DocumentGraph} fsJsonData
*/
function patchDataJson(diffData, fsJsonData) {
const { add, remove } = diffData

const clonedJsonData = [...fsJsonData]
const slugifyJsonMap = clonedJsonData.reduce((acc, cur) => {
acc[cur.slug] = cur
return acc
}, {})

for (const [slug, paths] of Object.entries(add)) {
if (!slugifyJsonMap[slug]) {
clonedJsonData.push({
paths,
slug,
title: slug,
})

continue
}

paths.forEach((path) => {
slugifyJsonMap[slug].paths.push(path)
})
}

for (const [slug, paths] of Object.entries(remove)) {
if (!slugifyJsonMap[slug]) continue

paths.forEach((path) => {
const index = slugifyJsonMap[slug].paths.findIndex(
(_path) => path === _path,
)
if (index > -1) {
slugifyJsonMap[slug].paths.splice(index, 1)
}
})
}

return clonedJsonData
}

/**
*
* @param {Record<string, import('./interface').SingleDocumentTree & {pathSet: Set<string>}} data
*/
function compareFsTreeWithExistJsonData(data) {
const diffAddPathMap = {}
const diffRemovePathMap = {}
const paths = globbySync(pathGlob, { onlyFiles: true })

const slugSetMap = {}
paths.forEach((fullPath) => {
const pathArr = fullPath.replace(workdirPath, '').split('/').filter(Boolean)

if (pathArr.length < 2) return
const slug = pathArr.shift()
if (!slug) return
slugSetMap[slug] ||= new Set()

slugSetMap[slug].add(`${pathArr.join('/')}`)
})

Object.keys(slugSetMap).map((slug) => {
if (!data[slug]) {
diffAddPathMap[slug] = Array.from(slugSetMap[slug])
return
}

slugSetMap[slug].forEach((path) => {
if (data[slug]?.pathSet.has(path)) {
return
}

diffAddPathMap[slug] ||= []
diffAddPathMap[slug].push(path)
})

data[slug]?.pathSet.forEach((path) => {
if (!slugSetMap[slug]) {
diffRemovePathMap[slug] = Array.from(data[slug].pathSet)
return
}

if (!slugSetMap[slug].has(path)) {
diffRemovePathMap[slug] ||= []
diffRemovePathMap[slug].push(path)
}
})
})

return {
add: diffAddPathMap,
remove: diffRemovePathMap,
}
}

function createDefaultDataJson() {
/**
* @type {import('./interface').DocumentGraph}
*/
const jsonData = []
/**
* @type {Record<string, import('./interface').SingleDocumentTree>}
*/
const slugToListMap = {}

globbySync(pathGlob, { onlyFiles: true }).map((fullPath) => {
const pathArr = fullPath.replace(workdirPath, '').split('/').filter(Boolean)

if (pathArr.length < 2) return
const slug = pathArr.shift()
if (!slug) return

let documentTree = slugToListMap[slug]

if (!documentTree) {
slugToListMap[slug] = {
slug,
paths: [],
title: slug,
}
documentTree = slugToListMap[slug]
}

documentTree.paths.push(`${pathArr.join('/')}`)

return pathArr
})

Object.values(slugToListMap).forEach((value) => {
jsonData.push(value)
})

writeFileSync(dataJsonPath, JSON.stringify(jsonData, null, 2), 'utf8')
}

function debounce(fn, wait) {
let callback = fn
let timerId = null

function debounced() {
let context = this

let args = arguments

clearTimeout(timerId)
timerId = setTimeout(function () {
callback.apply(context, args)
}, wait)
}

return debounced
}
Loading

0 comments on commit f6cebe9

Please sign in to comment.