1 const {test} = require('node:test')
2 const assert = require('node:assert/strict')
3 const {resolve, join, posix} = require('node:path')
4 const fs = require('node:fs/promises')
5 const os = require('node:os')
6 const pacote = require('pacote')
7 const yaml = require('yaml')
8 const semver = require('semver')
9
10 const navPath = resolve(__dirname, '..', '..', 'content', 'nav.yml')
11
12 const ANCHOR = __dirname
13 const SUT_CHAIN = [
14 require.resolve('../lib/build'),
15 require.resolve('../lib/extract'),
16 require.resolve('../lib/transform'),
17 ]
18
19 // Install module mocks into `require.cache`, saving any prior entry so it can
20 // be restored on cleanup. Cleanup also evicts the SUT chain so the next test
21 // re-evaluates `lib/build` against fresh mocks.
22 const installMocks = mocks => {
23 const saved = new Map()
24 const remember = path => {
25 if (!saved.has(path)) {
26 saved.set(path, require.cache[path])
27 }
28 }
29
30 for (const [key, exports] of Object.entries(mocks)) {
31 const resolved = require.resolve(key, {paths: [ANCHOR]})
32 remember(resolved)
33 require.cache[resolved] = {
34 id: resolved,
35 filename: resolved,
36 exports,
37 loaded: true,
38 children: [],
39 paths: [],
40 }
41 }
42
43 for (const path of SUT_CHAIN) {
44 remember(path)
45 delete require.cache[path]
46 }
47
48 return () => {
49 for (const [path, entry] of saved) {
50 if (entry === undefined) {
51 delete require.cache[path]
52 } else {
53 require.cache[path] = entry
54 }
55 }
56 }
57 }
58
59 // Minimal stand-in for tap's `t.testdir` — only handles the shapes our tests
60 // use: top-level string => file, top-level {} => empty directory.
61 const makeTestdir = async (t, contents) => {
62 const dir = await fs.mkdtemp(join(os.tmpdir(), 'cli-doc-test-'))
63 t.after(() => fs.rm(dir, {recursive: true, force: true}))
64
65 for (const [name, value] of Object.entries(contents)) {
66 const target = join(dir, name)
67 if (typeof value === 'string') {
68 await fs.writeFile(target, value, 'utf-8')
69 } else if (value && typeof value === 'object') {
70 await fs.mkdir(target, {recursive: true})
71 }
72 }
73 return dir
74 }
75
76 const getReleases = () => [
77 {
78 id: 'v6',
79 branch: 'release/v6',
80 },
81 {
82 id: 'v7',
83 branch: 'release/v7',
84 },
85 {
86 id: 'v8',
87 branch: 'release/v8',
88 },
89 {
90 id: 'v9',
91 branch: 'latest',
92 },
93 ]
94
95 const mockBuild = async (t, {releases = getReleases(), packument = {}, testdir: testdirOpts} = {}) => {
96 const rawNav = await fs.readFile(navPath, 'utf-8')
97 const nav = yaml.parse(rawNav)
98
99 const testdir = await makeTestdir(t, {
100 'nav.yml': rawNav,
101 content: {},
102 ...testdirOpts,
103 })
104
105 if (!packument.versions) {
106 packument.versions = releases.map(r => {
107 // real tarball requests are made for these verions
108 // so by default they all need to exist
109 switch (r.id.slice(1)) {
110 case '6':
111 return '6.14.18'
112 case '7':
113 return '7.24.2'
114 case '8':
115 return '8.19.3'
116 case '9':
117 return '9.0.0'
118 default:
119 throw new Error(`Unknown packument version: ${JSON.stringify(r)}`)
120 }
121 })
122 }
123
124 if (!packument.latest) {
125 packument.latest = packument.versions[packument.versions.length - 1]
126 }
127
128 const navSection = ref => {
129 const id = ref === 'latest' ? `v${semver.major(packument.latest)}` : posix.basename(ref)
130 const {variants} = nav.find(c => c.url === '/cli')
131 const {children} = variants.find(v => posix.basename(v.url) === id)
132 return yaml.stringify(children).replace(new RegExp(`/cli/${id}/`, 'g'), '/')
133 }
134
135 let shaCounter = 0
136 const restore = installMocks({
137 pacote: {
138 ...pacote,
139 packument: async () => {
140 return {
141 'dist-tags': {
142 latest: packument.latest,
143 },
144 versions: packument.versions.reduce((acc, v) => {
145 acc[v] = null
146 return acc
147 }, {}),
148 }
149 },
150 },
151 '@prettier/sync': {format: s => s},
152 '../lib/gh.js': {
153 getCurrentSha: async () => {
154 shaCounter = shaCounter + 1
155 return 'abc' + shaCounter
156 },
157 getFile: async ({ref}) => navSection(ref),
158 pathExists: async (ref, p) => {
159 if (ref.includes('v6') && p.includes('docs/lib/content')) {
160 return null
161 }
162 return p
163 },
164 nwo: `npm/cli`,
165 },
166 })
167 t.after(restore)
168
169 const build = require('../lib/build')
170
171 return {
172 testdir,
173 releases,
174 build: opts =>
175 build({
176 releases,
177 contentPath: join(testdir, 'content'),
178 navPath: join(testdir, 'nav.yml'),
179 ...opts,
180 }),
181 }
182 }
183
184 test('basic', async t => {
185 const {releases, build, testdir} = await mockBuild(t, {
186 testdir: {
187 'nav.yml': '- title: cli\n url: /cli',
188 },
189 })
190
191 await build()
192 assert.deepEqual(
193 await fs.readdir(join(testdir, 'content')),
194 releases.map(r => r.id),
195 )
196 })
197
198 test('prereleases', async t => {
199 const {build, releases, testdir} = await mockBuild(t, {
200 packument: {versions: ['6.14.18', '7.24.2', '8.19.3', '9.0.0-pre.2'], latest: '8.19.3'},
201 })
202
203 await build({prerelease: false})
204 const expectedReleases = releases.map(r => r.id).filter(r => r !== 'v9')
205 assert.deepEqual(await fs.readdir(join(testdir, 'content')), expectedReleases)
206
207 await build({prerelease: true})
208 assert.deepEqual(
209 await fs.readdir(join(testdir, 'content')),
210 releases.map(r => r.id),
211 )
212 })
213
214 test('earlier release is latest', async t => {
215 const {build} = await mockBuild(t, {
216 packument: {latest: '8.19.3'},
217 })
218
219 await build()
220 })
221
222 test('can skip fetching latest', async t => {
223 const {build} = await mockBuild(t)
224
225 await build({useCurrent: true})
226 })
227
228 test('add variant to nav', async t => {
229 const {build} = await mockBuild(t, {
230 testdir: {
231 'nav.yml': '- title: cli\n url: /cli\n variants:\n - url: /cli/v0',
232 },
233 })
234
235 await build()
236 })