1+ import type { RegistryMeta } from './registry'
2+
13import { Buffer } from 'node:buffer'
24import { execFileSync } from 'node:child_process'
5+ import { createHash } from 'node:crypto'
36import { mkdtempSync , readdirSync , readFileSync , rmSync , writeFileSync } from 'node:fs'
47
58import { parseYAML } from 'confbox/yaml'
@@ -41,6 +44,8 @@ const MARKDOWN_EXTENSION_RE = /\.md$/
4144const MAJOR_VERSION_RE = / ^ \d + $ /
4245const EXACT_VERSION_RE = / ^ \d + \. \d + \. \d + (?: - [ \w . ] + ) ? $ /
4346const MARKDOWN_SYNTAX_RE = / [ ` * _ [ \] ] | \{ [ ^ } ] * \} / g
47+ // eslint-disable-next-line no-control-regex
48+ const CONTROL_CHARS_RE = / [ \u0000 - \u001F \u007F - \u009F ] / g
4449
4550export const DOCS_BASE_URL = 'https://nuxt.com'
4651export const DOCS_PATH = '/docs'
@@ -67,20 +72,24 @@ const UNSAFE_KEY_RE = /[^\w.+-]/g
6772 */
6873export async function resolveDocsIndex ( cwd : string , options : DocsIndexProgress = { } ) : Promise < DocsIndex | undefined > {
6974 const nuxtVersion = await getNuxtVersion ( cwd ) . catch ( ( ) => undefined )
70- const cached = readCache ( nuxtVersion )
75+
76+ const local = localDocs ( cwd )
77+ if ( local && ( ! nuxtVersion || sameMajor ( local . version , nuxtVersion ) ) ) {
78+ return indexLocal ( local , options )
79+ }
80+
81+ const source = await detectNpmRegistry ( '@nuxt' , cwd )
82+ const cached = readCache ( nuxtVersion , source . registry )
7183 if ( cached ) {
7284 return cached
7385 }
7486
75- const local = localDocs ( cwd )
76- const index = local && ( ! nuxtVersion || sameMajor ( local . version , nuxtVersion ) )
77- ? indexLocal ( local , options )
78- : await fetchIndex ( cwd , nuxtVersion , options ) ?? ( local && indexLocal ( local , options ) )
79-
87+ const index = await fetchIndex ( source , nuxtVersion , options )
8088 if ( index ) {
81- writeCache ( nuxtVersion , index )
89+ writeCache ( nuxtVersion , source . registry , index )
90+ return index
8291 }
83- return index || undefined
92+ return local ? indexLocal ( local , options ) : undefined
8493}
8594
8695export interface DocsIndexProgress {
@@ -119,14 +128,14 @@ function localDocs(cwd: string): { root: string, version: string } | undefined {
119128 * predate the package or be a nightly, so the highest release sharing its major
120129 * (then whatever is latest) is accepted as a stand-in.
121130 */
122- async function fetchIndex ( cwd : string , nuxtVersion : string | undefined , options : DocsIndexProgress ) : Promise < DocsIndex | undefined > {
131+ async function fetchIndex ( source : RegistryMeta , nuxtVersion : string | undefined , options : DocsIndexProgress ) : Promise < DocsIndex | undefined > {
123132 const attempted = new Set < string > ( )
124133 for await ( const version of candidateVersions ( nuxtVersion ) ) {
125134 if ( attempted . has ( version ) ) {
126135 continue
127136 }
128137 attempted . add ( version )
129- const index = await downloadIndex ( cwd , version , options )
138+ const index = await downloadIndex ( source , version , options )
130139 if ( index ) {
131140 return index
132141 }
@@ -148,11 +157,11 @@ async function* candidateVersions(nuxtVersion: string | undefined): AsyncGenerat
148157 }
149158}
150159
151- async function downloadIndex ( cwd : string , version : string , options : DocsIndexProgress ) : Promise < DocsIndex | undefined > {
160+ async function downloadIndex ( source : RegistryMeta , version : string , options : DocsIndexProgress ) : Promise < DocsIndex | undefined > {
152161 options . onDownload ?.( version )
153162 const staging = mkdtempSync ( join ( getCacheDir ( CACHE_DIR ) , '.staging-' ) )
154163 try {
155- const tarball = await downloadTarball ( cwd , version )
164+ const tarball = await downloadTarball ( source , version )
156165 if ( ! tarball ) {
157166 return undefined
158167 }
@@ -197,8 +206,7 @@ function assertSafeArchiveMembers(archive: string): void {
197206 * itself: the docs are a public package, so a proxy that rejects this process is
198207 * no reason to give up on them.
199208 */
200- async function downloadTarball ( cwd : string , version : string ) : Promise < Buffer | undefined > {
201- const { registry, authorization } = await detectNpmRegistry ( '@nuxt' , cwd )
209+ async function downloadTarball ( { registry, authorization } : RegistryMeta , version : string ) : Promise < Buffer | undefined > {
202210 const sources : [ registry : string , authorization : string | null ] [ ] = [ [ registry , authorization ] ]
203211 if ( registry !== PUBLIC_REGISTRY ) {
204212 sources . push ( [ PUBLIC_REGISTRY , null ] )
@@ -268,28 +276,31 @@ function buildIndex(root: string, version: string): DocsIndex | undefined {
268276/**
269277 * Keyed by the Nuxt version asked about rather than the docs version resolved for
270278 * it, so a project on a Nuxt release with no docs of its own does not repeat the
271- * registry lookup that found the stand-in.
279+ * registry lookup that found the stand-in. The registry is part of the key too:
280+ * a project can name its own in `.npmrc`, and what that serves should not be
281+ * offered as the docs in projects that use a different one.
272282 */
273- function cacheFile ( nuxtVersion : string | undefined ) : string {
283+ function cacheFile ( nuxtVersion : string | undefined , registry : string ) : string {
274284 // A version can be anything a `package.json` or a dependency specifier holds, so
275285 // it is reduced to a file name rather than trusted as one.
276286 const key = ( nuxtVersion || 'latest' ) . replace ( UNSAFE_KEY_RE , '_' )
277- return join ( getCacheDir ( CACHE_DIR ) , `index-${ key } .json` )
287+ const suffix = registry === PUBLIC_REGISTRY ? '' : `-${ createHash ( 'sha256' ) . update ( registry ) . digest ( 'hex' ) . slice ( 0 , 12 ) } `
288+ return join ( getCacheDir ( CACHE_DIR ) , `index-${ key } ${ suffix } .json` )
278289}
279290
280- function readCache ( nuxtVersion : string | undefined ) : DocsIndex | undefined {
291+ function readCache ( nuxtVersion : string | undefined , registry : string ) : DocsIndex | undefined {
281292 try {
282- const cached = JSON . parse ( readFileSync ( cacheFile ( nuxtVersion ) , 'utf8' ) ) as DocsIndex
293+ const cached = JSON . parse ( readFileSync ( cacheFile ( nuxtVersion , registry ) , 'utf8' ) ) as DocsIndex
283294 return cached . entries ?. length > 0 ? cached : undefined
284295 }
285296 catch {
286297 return undefined
287298 }
288299}
289300
290- function writeCache ( nuxtVersion : string | undefined , index : DocsIndex ) : void {
301+ function writeCache ( nuxtVersion : string | undefined , registry : string , index : DocsIndex ) : void {
291302 try {
292- writeFileSync ( cacheFile ( nuxtVersion ) , JSON . stringify ( index ) , 'utf8' )
303+ writeFileSync ( cacheFile ( nuxtVersion , registry ) , JSON . stringify ( index ) , 'utf8' )
293304 }
294305 catch ( error ) {
295306 debug ( 'Could not cache the documentation index:' , error )
@@ -323,11 +334,12 @@ function readEntry(root: string, file: string): DocsEntry | undefined {
323334 return undefined
324335 }
325336
326- const path = toSitePath ( relative ( root , file ) )
327- const title = data . title ?. trim ( ) || path . split ( '/' ) . pop ( ) || path
337+ // Everything indexed ends up on the terminal, so escape sequences are dropped.
338+ const path = toSitePath ( relative ( root , file ) ) . replace ( CONTROL_CHARS_RE , '' )
339+ const title = data . title ?. replace ( CONTROL_CHARS_RE , '' ) . trim ( ) || path . split ( '/' ) . pop ( ) || path
328340 return {
329341 title,
330- description : data . description ?. trim ( ) ,
342+ description : data . description ?. replace ( CONTROL_CHARS_RE , '' ) . trim ( ) ,
331343 path,
332344 headings : [ ...source . matchAll ( HEADING_RE ) ] . map ( match => clean ( match [ 1 ] ! ) ) ,
333345 }
0 commit comments