1 const parseFm = require('front-matter')
2 const {Minipass} = require('minipass')
3 const yaml = require('yaml')
4 const {minimatch} = require('minimatch')
5 const {sep, join, posix, isAbsolute} = require('path')
6 const gh = require('./gh')
7 const prettier = require('@prettier/sync')
8 const rawRedirects = require('./redirects')
9
10 const prettierFormat = str =>
11 prettier.format(str, {
12 parser: 'markdown',
13 proseWrap: 'never',
14 })
15
16 const getPathParts = path => {
17 const abs = isAbsolute(path)
18 const paths = path.replace(/\.mdx?$/, '').split(sep)
19
20 const pathId = posix.join(abs ? '/' : '', ...paths)
21 const page = posix.basename(pathId)
22 const section = posix.dirname(pathId)
23
24 return {
25 path: pathId,
26 page,
27 section: section === '.' ? '' : section,
28 }
29 }
30
31 const getRedirects = ({path, release}) => {
32 const redirects = [getPathParts(path)]
33 const [pagePath] = redirects
34 const canonical = posix.join(release.url, pagePath.section, pagePath.page === 'index' ? '' : pagePath.page)
35
36 for (const [k, v] of Object.entries(rawRedirects).reverse()) {
37 const pageRedirects = redirects.flatMap(redirect => {
38 if (minimatch(redirect.path, k)) {
39 return v({...getPathParts(redirect.path), release}).map(p => ({
40 ...redirect,
41 ...(typeof p === 'object' ? p : {path: p}),
42 }))
43 }
44 return []
45 })
46 redirects.push(...pageRedirects.filter(Boolean))
47 }
48
49 const prefixRedirects = redirects
50 .flatMap(redirect => {
51 const useDefault = redirect.default || release.default
52 if (isAbsolute(redirect.path)) {
53 return useDefault ? redirect.path : null
54 } else {
55 return release.urlPrefixes.flatMap(p => [
56 posix.join('/', p, release.id, redirect.path),
57 useDefault ? posix.join('/', p, redirect.path) : null,
58 ])
59 }
60 })
61 .filter(Boolean)
62 .filter(r => r !== canonical)
63
64 return [...new Set(prefixRedirects)].sort((a, b) => a.localeCompare(b, 'en'))
65 }
66
67 const transform = (data, {release, path, frontmatter, format = s => s}) => {
68 let {attributes, body} = parseFm(data.toString())
69
70 /* istanbul ignore next */
71 if (!attributes.redirect_from) {
72 attributes.redirect_from = []
73 }
74 attributes.redirect_from.push(...getRedirects({path, release}))
75
76 const ghFrontmatter = {
77 github_repo: gh.nwo,
78 github_branch: release.branch,
79 github_path: join(release.src, path).split(sep).join(posix.sep),
80 }
81
82 const order = [
83 'title',
84 'shortName',
85 'section',
86 'description',
87 'github_repo',
88 'github_branch',
89 'github_path',
90 'redirect_from',
91 ]
92
93 const sortRedirects = (a, b) => a.localeCompare(b, 'en')
94
95 attributes = Object.fromEntries(
96 Object.entries({...attributes, ...ghFrontmatter, ...frontmatter})
97 .filter(([, v]) => (Array.isArray(v) ? v.length : true))
98 .map(([k, v]) => (Array.isArray(v) ? [k, v.sort(sortRedirects)] : [k, v]))
99 .sort(([a], [b]) => {
100 /* istanbul ignore next */
101 const aIndex = order.includes(a) ? order.indexOf(a) : order.length
102 /* istanbul ignore next */
103 const bIndex = order.includes(b) ? order.indexOf(b) : order.length
104 return aIndex - bIndex
105 }),
106 )
107
108 // first format with prettier, this helps so other replacements don't have to
109 // worry about newlines vs spaces
110 body = prettierFormat(body)
111
112 // then do replacements for all cli makdown files
113 body = body
114 // some legacy versions of the docs did not get this replaced
115 // in the source so we need to replace it here
116 .replace(/@VERSION@/g, release.version)
117 // also replace all internal markdown links with links to this
118 // specific version
119 .replace(
120 /\[([^\]]+)\]\(\/((?:commands|configuring-npm|using-npm)\/[^)]+)\)/g,
121 (_, p1, p2) => `[${p1}](${release.url}/${p2})`,
122 )
123 // remove html comments which are not mdx compatible
124 .replace(/^<!--\s.*?\s-->$\n/gm, '')
125 // replace markdown autolinks with full markdown links, also for mdx
126 .replace(/<(http)(.*?)\\?>/g, '[$1$2]($1$2)')
127
128 // then do any transformer specific replacements
129 body = format(body)
130
131 // then do any transformer specific replacements
132 body = format(body)
133
134 // prettier on the final assembled output so the committed file passes prettier --check
135 return prettierFormat(`---\n${yaml.stringify(attributes).trim()}\n---\n\n${body}`)
136 }
137
138 // copied from minipass-collect. collects all chunks into a single
139 // buffer which is then transformed before a final write event
140 class Transform extends Minipass {
141 #data = []
142 #length = 0
143 #opts = {}
144
145 static sync = transform
146
147 constructor(transformOpts) {
148 super({encoding: 'utf-8'})
149 this.#opts = transformOpts
150 }
151
152 write(c) {
153 this.#data.push(c)
154 this.#length += c.length
155 return true
156 }
157
158 end() {
159 super.write(transform(Buffer.concat(this.#data, this.#length), this.#opts))
160 return super.end()
161 }
162 }
163
164 module.exports = Transform