forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcreate-tree.js
163 lines (151 loc) · 6.29 KB
/
create-tree.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import path from 'path'
import fs from 'fs/promises'
import Page from './page.js'
export default async function createTree(originalPath, rootPath, previousTree) {
const basePath = rootPath || originalPath
// On recursive runs, this is processing page.children items in `/<link>` format.
// If the path exists as is, assume this is a directory with a child index.md.
// Otherwise, assume it's a child .md file and add `.md` to the path.
let filepath
let mtime
// This kills two birds with one stone. We (attempt to) read it as a file,
// to find out if it's a directory or a file and whence we know that
// we also collect it's modification time.
try {
filepath = `${originalPath}.md`
mtime = await getMtime(filepath)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
filepath = `${originalPath}/index.md`
// Note, if this throws, that's quite fine. It usually means that
// there's a `index.md` whose `children:` entry lists something that
// doesn't exist on disk. So the writer who tries to preview the
// page will see the error and it's hopefully clear what's actually
// wrong.
try {
mtime = await getMtime(filepath)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
// Throw an error if we can't find a content file associated with the children: entry.
// But don't throw an error if the user is running the site locally and hasn't cloned the Early Access repo.
if (originalPath === path.join('content', 'early-access')) {
return
}
throw new Error(
`Cannot find a content file at ${originalPath}. Fix the children frontmatter entry "/${path.basename(
originalPath,
)}" in ${path.dirname(originalPath)}/index.md.\n`,
)
}
}
const relativePath = filepath.replace(`${basePath}/`, '')
// Reading in a file from disk is slow and best avoided if we can be
// certain it isn't necessary. If the previous tree is known and that
// tree's page node's `mtime` hasn't changed, we can use that instead.
let page
if (previousTree && previousTree.page.mtime === mtime) {
// A save! We can use the same exact Page instance from the previous
// tree because the assumption is that since the `.md` file it was
// created from hasn't changed (on disk) the instance object wouldn't
// change.
page = previousTree.page
} else {
// Either the previous tree doesn't exist yet or the modification time
// of the file on disk has changed.
page = await Page.init({
basePath,
relativePath,
languageCode: 'en',
mtime,
})
}
if (!page) {
throw Error(`Cannot initialize page for ${filepath}`)
}
// Create the root tree object on the first run, and create children recursively.
const item = {
page,
// This is only here for the sake of reloading the tree later which
// only happens in development mode.
// The reloading of the tree compares the list of children (array of
// strings) with what it might have been in the previous tree.
// Then it can use the "n'th" access to figure out what the
// "previous sub tree" was for each child.
// So if a writer edits the 'children:' frontmatter property
// this value now will be different from what it was before.
// It's not enough to rely on *length* of the array before and after
// because the change could have been to remove one and add another.
children: page.children,
}
// Process frontmatter children recursively.
if (item.page.children) {
assertUniqueChildren(item.page)
item.childPages = (
await Promise.all(
item.page.children.map(async (child, i) => {
let childPreviousTree
if (previousTree && previousTree.childPages) {
if (equalArray(item.page.children, previousTree.children)) {
// We can only safely rely on picking the same "n'th" item
// from the array if we're confident the names are the same
// as they were before.
// Otherwise, suppose you add an entry to `children:`
// and add another, then length would be the same but
// each position might relate to different child.
childPreviousTree = previousTree.childPages[i]
}
}
const subTree = await createTree(
path.posix.join(originalPath, child),
basePath,
childPreviousTree,
)
if (!subTree) {
// Remove that children.
// For example, the 'early-access' might have been in the
// `children:` property but it was decided to be skipped
// (early exit instead of returning a tree). So let's
// mutate the `page.children` so we can benefit from the
// ability to reload the site tree on consective requests.
item.page.children = item.page.children.filter((c) => c !== child)
}
return subTree
}),
)
).filter(Boolean)
}
return item
}
function equalArray(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, i) => value === arr2[i])
}
async function getMtime(filePath) {
// Use mtimeMs, which is a regular floating point number, instead of the
// mtime which is a Date based on that same number.
// Otherwise, if we use the Date instances, we have to compare
// them using `oneDate.getTime() === anotherDate.getTime()`.
const { mtimeMs } = await fs.stat(filePath)
// The `mtimeMs` is a number like `1669827766942.7954`
// From the docs:
// "The timestamp indicating the last time this file was modified expressed
// in nanoseconds since the POSIX Epoch."
// But the number isn't actually all that important. We just need it to
// later be able to know if it changed. We round it to the nearest
// millisecond.
return Math.round(mtimeMs)
}
function assertUniqueChildren(page) {
if (page.children.length !== new Set(page.children).size) {
const count = {}
page.children.forEach((entry) => (count[entry] = 1 + (count[entry] || 0)))
let msg = `${page.relativePath} has duplicates in the 'children' key.`
for (const [entry, times] of Object.entries(count)) {
if (times > 1) msg += ` '${entry}' is repeated ${times} times. `
}
throw new Error(msg)
}
}