@antora/content-aggregator 3.0.0-alpha.9 → 3.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The Content Aggregator is a component in Antora responsible for fetching and aggregating content distributed across multiple local and remote git repositories for use in an Antora documentation pipeline.
4
4
 
5
5
  [Antora](https://antora.org) is a modular static site generator designed for creating documentation sites from AsciiDoc documents.
6
- Its site generator pipeline aggregates documents from versioned content repositories and processes them using [Asciidoctor](https://asciidoctor.org).
6
+ Its site generator aggregates documents from versioned content repositories and processes them using [Asciidoctor](https://asciidoctor.org).
7
7
 
8
8
  ## Copyright and License
9
9
 
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- if (!Promise.allSettled) require('./promise-all-settled-polyfill')
4
-
5
3
  const camelCaseKeys = require('camelcase-keys')
6
4
  const { createHash } = require('crypto')
7
5
  const createHttpPlugin = require('./git-plugin-http')
@@ -9,6 +7,7 @@ const decodeUint8Array = require('./decode-uint8-array')
9
7
  const EventEmitter = require('events')
10
8
  const expandPath = require('@antora/expand-path-helper')
11
9
  const File = require('./file')
10
+ const filterRefs = require('./filter-refs')
12
11
  const flattenDeep = require('./flatten-deep')
13
12
  const fs = require('fs')
14
13
  const { promises: fsp } = fs
@@ -16,18 +15,17 @@ const getCacheDir = require('cache-directory')
16
15
  const GitCredentialManagerStore = require('./git-credential-manager-store')
17
16
  const git = require('./git')
18
17
  const { NotFoundError, ObjectTypeError, UnknownTransportError, UrlParseError } = git.Errors
19
- const invariably = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
20
- const { makeRe: makePicomatchRx } = require('picomatch')
21
- const matcher = require('matcher')
22
- const MultiProgress = require('multi-progress')
18
+ const globStream = require('glob-stream')
19
+ const invariably = require('./invariably')
20
+ const { makeMatcherRx, versionMatcherOpts: VERSION_MATCHER_OPTS } = require('./matcher')
21
+ const MultiProgress = require('multi-progress') // calls require('progress') as a peer dependencies
23
22
  const ospath = require('path')
24
23
  const { posix: path } = ospath
25
24
  const posixify = ospath.sep === '\\' ? (p) => p.replace(/\\/g, '/') : undefined
26
25
  const { fs: resolvePathGlobsFs, git: resolvePathGlobsGit } = require('./resolve-path-globs')
27
- const { Transform } = require('stream')
28
- const map = (transform, flush = undefined) => new Transform({ objectMode: true, transform, flush })
26
+ const { pipeline, Writable } = require('stream')
27
+ const forEach = (write) => new Writable({ objectMode: true, write })
29
28
  const userRequire = require('@antora/user-require-helper')
30
- const vfs = require('vinyl-fs')
31
29
  const yaml = require('js-yaml')
32
30
 
33
31
  const {
@@ -39,11 +37,10 @@ const {
39
37
  GIT_CORE,
40
38
  GIT_OPERATION_LABEL_LENGTH,
41
39
  GIT_PROGRESS_PHASES,
42
- PICOMATCH_VERSION_OPTS,
40
+ REF_PATTERN_CACHE_KEY,
43
41
  SYMLINK_FILE_MODE,
44
42
  VALID_STATE_FILENAME,
45
43
  } = require('./constants')
46
-
47
44
  const ANY_SEPARATOR_RX = /[:/]/
48
45
  const CSV_RX = /\s*,\s*/
49
46
  const VENTILATED_CSV_RX = /\s*,\s+/
@@ -101,7 +98,8 @@ function aggregateContent (playbook) {
101
98
  return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
102
99
  }, new Map())
103
100
  const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
104
- const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir }
101
+ const refPatternCache = Object.assign(new Map(), { braces: new Map() })
102
+ const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
105
103
  return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
106
104
  progress && progress.terminate()
107
105
  throw err
@@ -122,14 +120,14 @@ async function collectFiles (sourcesByUrl, loadOpts, concurrency) {
122
120
  })
123
121
  ),
124
122
  ])
125
- let rejected, started
123
+ let rejection, started
126
124
  const startedContinuations = []
127
125
  const recordRejection = (err) => {
128
- throw (rejected = true) && err
126
+ rejection = err
129
127
  }
130
128
  const runTask = (primary, continuation, idx) =>
131
129
  primary().then((value) => {
132
- if (!rejected) startedContinuations[idx] = continuation(value).catch(recordRejection)
130
+ if (!rejection) startedContinuations[idx] = continuation(value).catch(recordRejection)
133
131
  }, recordRejection)
134
132
  if (tasks.length > concurrency) {
135
133
  started = []
@@ -140,17 +138,16 @@ async function collectFiles (sourcesByUrl, loadOpts, concurrency) {
140
138
  )
141
139
  started.push(current)
142
140
  if (pending.push(current) < concurrency) continue
143
- if (await Promise.race(pending).then(invariably.true, invariably.false)) continue
144
- break
141
+ await Promise.race(pending)
142
+ if (rejection) break
145
143
  }
146
144
  } else {
147
145
  started = tasks.map(([primary, continuation], idx) => runTask(primary, continuation, idx))
148
146
  }
149
- return Promise.allSettled(started).then((outcomes) =>
150
- Promise.allSettled(startedContinuations).then((continuationOutcomes) => {
151
- const rejection = outcomes.push(...continuationOutcomes) && outcomes.find(({ status }) => status === 'rejected')
152
- if (rejection) throw rejection.reason
153
- return continuationOutcomes.map(({ value }) => value)
147
+ return Promise.all(started).then(() =>
148
+ Promise.all(startedContinuations).then((result) => {
149
+ if (rejection) throw rejection
150
+ return result
154
151
  })
155
152
  )
156
153
  }
@@ -173,13 +170,14 @@ function buildAggregate (componentVersionBuckets) {
173
170
 
174
171
  async function loadRepository (url, opts) {
175
172
  let authStatus, dir, repo
173
+ const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
176
174
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
177
175
  let credentials, displayUrl
178
176
  ;({ displayUrl, url, credentials } = extractCredentials(url))
179
177
  const { cacheDir, fetch, fetchTags, gitPlugins, progress } = opts
180
178
  dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
181
179
  // NOTE the presence of the url property on the repo object implies the repository is remote
182
- repo = { cache: {}, dir, fs, gitdir: dir, noCheckout: true, url }
180
+ repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
183
181
  const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
184
182
  try {
185
183
  await fsp.access(validStateFile)
@@ -204,7 +202,7 @@ async function loadRepository (url, opts) {
204
202
  authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
205
203
  }
206
204
  } catch (gitErr) {
207
- await rmdir(dir)
205
+ await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
208
206
  if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
209
207
  const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'clone')
210
208
  await git
@@ -225,15 +223,12 @@ async function loadRepository (url, opts) {
225
223
  }
226
224
  } else if (await isDirectory((dir = expandPath(url, { dot: opts.startDir })))) {
227
225
  const gitdir = ospath.join(dir, '.git')
228
- repo = (await isDirectory(gitdir))
229
- ? { cache: {}, dir, fs, gitdir }
230
- : { cache: {}, dir, fs, gitdir: dir, noCheckout: true }
226
+ repo = (await isDirectory(gitdir)) ? { cache, dir, fs, gitdir } : { cache, dir, fs, gitdir: dir, noCheckout: true }
231
227
  try {
232
228
  await git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo))
233
229
  } catch {
234
- throw new Error(
235
- `Local content source must be a git repository: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`
236
- )
230
+ const msg = `Local content source must be a git repository: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`
231
+ throw new Error(msg)
237
232
  }
238
233
  } else {
239
234
  throw new Error(`Local content source does not exist: ${dir}${url !== dir ? ' (url: ' + url + ')' : ''}`)
@@ -271,99 +266,102 @@ async function collectFilesFromSource (source, repo, remoteName, authStatus) {
271
266
  async function selectReferences (source, repo, remote) {
272
267
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns = '.' } = source
273
268
  const isBare = repo.noCheckout
269
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
274
270
  const noWorktree = repo.url ? undefined : null
275
271
  const refs = new Map()
276
- if (tagPatterns) {
277
- tagPatterns = Array.isArray(tagPatterns)
272
+ if (
273
+ tagPatterns &&
274
+ (tagPatterns = Array.isArray(tagPatterns)
278
275
  ? tagPatterns.map((pattern) => String(pattern))
279
- : String(tagPatterns).split(CSV_RX)
280
- if (tagPatterns.length) {
281
- const tags = await git.listTags(repo)
282
- for (const shortname of tags.length ? matcher(tags, tagPatterns) : tags) {
276
+ : splitRefPatterns(String(tagPatterns))).length
277
+ ) {
278
+ const tags = await git.listTags(repo)
279
+ if (tags.length) {
280
+ for (const shortname of filterRefs(tags, tagPatterns, patternCache)) {
283
281
  // NOTE tags are stored using symbol keys to distinguish them from branches
284
282
  refs.set(Symbol(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
285
283
  }
286
284
  }
287
285
  }
288
- if (branchPatterns) {
289
- if (worktreePatterns) {
290
- if (worktreePatterns === '.') {
291
- worktreePatterns = ['.']
292
- } else if (worktreePatterns === true) {
293
- worktreePatterns = ['.', '*']
294
- } else {
295
- worktreePatterns = Array.isArray(worktreePatterns)
296
- ? worktreePatterns.map((pattern) => String(pattern))
297
- : String(worktreePatterns).split(CSV_RX)
298
- }
286
+ if (!branchPatterns) return [...refs.values()]
287
+ if (worktreePatterns) {
288
+ if (worktreePatterns === '.') {
289
+ worktreePatterns = ['.']
290
+ } else if (worktreePatterns === true) {
291
+ worktreePatterns = ['.', '*']
292
+ } else {
293
+ worktreePatterns = Array.isArray(worktreePatterns)
294
+ ? worktreePatterns.map((pattern) => String(pattern))
295
+ : splitRefPatterns(String(worktreePatterns))
299
296
  }
300
- const branchPatternsString = String(branchPatterns)
301
- if (branchPatternsString === 'HEAD' || branchPatternsString === '.') {
297
+ }
298
+ const branchPatternsString = String(branchPatterns)
299
+ if (branchPatternsString === 'HEAD' || branchPatternsString === '.') {
300
+ const currentBranch = await getCurrentBranchName(repo, remote)
301
+ if (currentBranch) {
302
+ branchPatterns = [currentBranch]
303
+ } else if (isBare) {
304
+ return [...refs.values()]
305
+ } else {
306
+ // NOTE current branch is undefined when HEAD is detached
307
+ const head = worktreePatterns[0] === '.' ? repo.dir : noWorktree
308
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
309
+ return [...refs.values()]
310
+ }
311
+ } else if (
312
+ (branchPatterns = Array.isArray(branchPatterns)
313
+ ? branchPatterns.map((pattern) => String(pattern))
314
+ : splitRefPatterns(branchPatternsString)).length
315
+ ) {
316
+ let headBranchIdx
317
+ // NOTE we can assume at least two entries if HEAD or . are present
318
+ if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
302
319
  const currentBranch = await getCurrentBranchName(repo, remote)
303
320
  if (currentBranch) {
304
- branchPatterns = [currentBranch]
305
- } else {
306
- if (!isBare) {
307
- // NOTE current branch is undefined when HEAD is detached
308
- const head = worktreePatterns[0] === '.' ? repo.dir : noWorktree
309
- refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
310
- }
311
- return [...refs.values()]
312
- }
313
- } else if (
314
- (branchPatterns = Array.isArray(branchPatterns)
315
- ? branchPatterns.map((pattern) => String(pattern))
316
- : branchPatternsString.split(CSV_RX)).length
317
- ) {
318
- let headBranchIdx
319
- // NOTE we can assume at least two entries if HEAD or . are present
320
- if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
321
- const currentBranch = await getCurrentBranchName(repo, remote)
322
- if (currentBranch) {
323
- // NOTE ignore if current branch is already in list
324
- if (~branchPatterns.indexOf(currentBranch)) {
325
- branchPatterns.splice(headBranchIdx, 1)
326
- } else {
327
- branchPatterns[headBranchIdx] = currentBranch
328
- }
329
- } else {
330
- if (!isBare) {
331
- let head = noWorktree
332
- if (worktreePatterns[0] === '.') {
333
- worktreePatterns = worktreePatterns.slice(1)
334
- head = repo.dir
335
- }
336
- // NOTE current branch is undefined when HEAD is detached
337
- refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
338
- }
321
+ // NOTE ignore if current branch is already in list
322
+ if (~branchPatterns.indexOf(currentBranch)) {
339
323
  branchPatterns.splice(headBranchIdx, 1)
324
+ } else {
325
+ branchPatterns[headBranchIdx] = currentBranch
340
326
  }
327
+ } else if (isBare) {
328
+ branchPatterns.splice(headBranchIdx, 1)
329
+ } else {
330
+ let head = noWorktree
331
+ if (worktreePatterns[0] === '.') {
332
+ worktreePatterns = worktreePatterns.slice(1)
333
+ head = repo.dir
334
+ }
335
+ // NOTE current branch is undefined when HEAD is detached
336
+ refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
337
+ branchPatterns.splice(headBranchIdx, 1)
341
338
  }
342
- } else {
343
- return [...refs.values()]
344
339
  }
345
- // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
346
- const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
347
- if (remoteBranches.length) {
348
- for (const shortname of matcher(remoteBranches, branchPatterns)) {
349
- const fullname = 'remotes/' + remote + '/' + shortname
350
- refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
351
- }
340
+ } else {
341
+ return [...refs.values()]
342
+ }
343
+ // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
344
+ const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
345
+ if (remoteBranches.length) {
346
+ for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
347
+ const fullname = 'remotes/' + remote + '/' + shortname
348
+ refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
352
349
  }
353
- // NOTE only consider local branches if repo has a worktree or there are no remote tracking branches
354
- if (!isBare) {
355
- const localBranches = await git.listBranches(repo)
356
- if (localBranches.length) {
357
- const worktrees = await findWorktrees(repo, worktreePatterns)
358
- for (const shortname of matcher(localBranches, branchPatterns)) {
359
- const head = worktrees.get(shortname) || noWorktree
360
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
361
- }
350
+ }
351
+ // NOTE only consider local branches if repo has a worktree or there are no remote tracking branches
352
+ if (!isBare) {
353
+ const localBranches = await git.listBranches(repo)
354
+ if (localBranches.length) {
355
+ const worktrees = await findWorktrees(repo, worktreePatterns)
356
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
357
+ const head = worktrees.get(shortname) || noWorktree
358
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
362
359
  }
363
- } else if (!remoteBranches.length) {
364
- // QUESTION should local branches be used if the only remote branch is HEAD?
365
- const localBranches = await git.listBranches(repo)
366
- for (const shortname of localBranches.length ? matcher(localBranches, branchPatterns) : localBranches) {
360
+ }
361
+ } else if (!remoteBranches.length) {
362
+ const localBranches = await git.listBranches(repo)
363
+ if (localBranches.length) {
364
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
367
365
  refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
368
366
  }
369
367
  }
@@ -437,28 +435,11 @@ function collectFilesFromStartPath (startPath, repo, authStatus, ref, worktreePa
437
435
  }
438
436
 
439
437
  function readFilesFromWorktree (worktreePath, startPath) {
440
- const cwd = ospath.join(worktreePath, startPath)
438
+ const cwd = ospath.join(worktreePath, startPath, '.') // . shaves off trailing slash
441
439
  return fsp.stat(cwd).then(
442
- (stat) => {
443
- if (!stat.isDirectory()) throw new Error(`the start path '${startPath}' is not a directory`)
444
- return new Promise((resolve, reject) =>
445
- vfs
446
- .src(CONTENT_SRC_GLOB, Object.assign({ cwd }, CONTENT_SRC_OPTS))
447
- .on('error', (err) => {
448
- if (err.code === 'ENOENT' && err.syscall === 'stat') {
449
- try {
450
- if (fs.lstatSync(err.path).isSymbolicLink()) {
451
- err.message = `Broken symbolic link detected at ${ospath.relative(cwd, err.path)}`
452
- }
453
- } catch {}
454
- } else if (err.code === 'ELOOP') {
455
- err.message = `Symbolic link cycle detected at ${ospath.relative(cwd, err.path)}`
456
- }
457
- reject(err)
458
- })
459
- .pipe(relativizeFiles())
460
- .pipe(collectDataFromStream(resolve))
461
- )
440
+ (startPathStat) => {
441
+ if (!startPathStat.isDirectory()) throw new Error(`the start path '${startPath}' is not a directory`)
442
+ return srcFs(cwd)
462
443
  },
463
444
  () => {
464
445
  throw new Error(`the start path '${startPath}' does not exist`)
@@ -466,40 +447,43 @@ function readFilesFromWorktree (worktreePath, startPath) {
466
447
  )
467
448
  }
468
449
 
469
- /**
470
- * Transforms the path of every file in the stream to a relative posix path.
471
- *
472
- * Applies a mapping function to all files in the stream so they end up with a
473
- * posixified path relative to the file's base instead of the filesystem root.
474
- * This mapper also filters out any directories (indicated by file.isNull())
475
- * that got caught up in the glob.
476
- */
477
- function relativizeFiles () {
478
- return map((file, enc, next) => {
479
- if (file.isNull()) {
480
- next()
481
- } else {
482
- next(
483
- null,
484
- new File({
485
- path: posixify ? posixify(file.relative) : file.relative,
486
- contents: file.contents,
487
- stat: file.stat,
488
- src: { abspath: file.path },
489
- })
490
- )
491
- }
492
- })
493
- }
494
-
495
- function collectDataFromStream (done) {
496
- const accum = []
497
- return map(
498
- (obj, _, next) => {
499
- accum.push(obj)
500
- next()
501
- },
502
- () => done(accum)
450
+ function srcFs (cwd) {
451
+ const relpathStart = cwd.length + 1
452
+ return new Promise((resolve, reject, cache = Object.create(null), files = []) =>
453
+ pipeline(
454
+ globStream(CONTENT_SRC_GLOB, Object.assign({ cache, cwd }, CONTENT_SRC_OPTS)),
455
+ forEach(({ path: abspathPosix }, _, done) => {
456
+ if (Array.isArray(cache[abspathPosix])) return done() // detects some directories, but not all
457
+ const abspath = posixify ? ospath.normalize(abspathPosix) : abspathPosix
458
+ const relpath = abspath.substr(relpathStart)
459
+ symlinkAwareStat(abspath).then(
460
+ (stat) => {
461
+ if (stat.isDirectory()) return done() // detects remaining directories
462
+ fsp.readFile(abspath).then(
463
+ (contents) => {
464
+ files.push(new File({ path: posixify ? posixify(relpath) : relpath, contents, stat, src: { abspath } }))
465
+ done()
466
+ },
467
+ (readErr) => {
468
+ done(Object.assign(readErr, { message: readErr.message.replace(`'${abspath}'`, relpath) }))
469
+ }
470
+ )
471
+ },
472
+ (statErr) => {
473
+ if (statErr.symlink) {
474
+ statErr.message =
475
+ statErr.code === 'ELOOP'
476
+ ? `Symbolic link cycle detected at ${relpath}`
477
+ : `Broken symbolic link detected at ${relpath}`
478
+ } else {
479
+ statErr.message = statErr.message.replace(`'${abspath}'`, relpath)
480
+ }
481
+ done(statErr)
482
+ }
483
+ )
484
+ }),
485
+ (err) => (err ? reject(err) : resolve(files))
486
+ )
503
487
  )
504
488
  }
505
489
 
@@ -601,7 +585,7 @@ function readGitSymlink (repo, root, parent, { oid }, following) {
601
585
  let targetParent
602
586
  if (parent.dirname) {
603
587
  const dirname = parent.dirname + '/'
604
- target = path.join(dirname, target)
588
+ target = path.join(dirname, target) // join doesn't remove trailing separator
605
589
  if (target.startsWith(dirname)) {
606
590
  target = target.substr(dirname.length)
607
591
  targetParent = parent
@@ -609,10 +593,12 @@ function readGitSymlink (repo, root, parent, { oid }, following) {
609
593
  targetParent = root
610
594
  }
611
595
  } else {
612
- target = path.normalize(target)
596
+ target = path.normalize(target) // normalize doesn't remove trailing separator
613
597
  targetParent = root
614
598
  }
615
- return readGitObjectAtPath(repo, root, targetParent, target.split('/'), following)
599
+ const targetSegments = target.split('/')
600
+ if (!targetSegments[targetSegments.length - 1]) targetSegments.pop()
601
+ return readGitObjectAtPath(repo, root, targetParent, targetSegments, following)
616
602
  })
617
603
  }
618
604
  const err = { name: 'SymbolicLinkCycleError', code: 'SymbolicLinkCycleError', oid }
@@ -688,7 +674,7 @@ function loadComponentDescriptor (files, ref, version) {
688
674
  matched = version[refname]
689
675
  } else if (
690
676
  !Object.entries(version).some(([pattern, replacement]) => {
691
- const result = refname.replace(makePicomatchRx(pattern, PICOMATCH_VERSION_OPTS), '\0' + replacement)
677
+ const result = refname.replace(makeMatcherRx(pattern, VERSION_MATCHER_OPTS), '\0' + replacement)
692
678
  if (result === refname) return false
693
679
  matched = result.substr(1)
694
680
  return true
@@ -918,34 +904,13 @@ function isDirectory (url) {
918
904
  return fsp.stat(url).then((stat) => stat.isDirectory(), invariably.false)
919
905
  }
920
906
 
921
- /**
922
- * Removes the specified directory (including all of its contents) or file.
923
- * Equivalent to fs.promises.rmdir(dir, { recursive: true }) in Node 12.
924
- */
925
- function rmdir (dir) {
926
- return fsp
927
- .readdir(dir, { withFileTypes: true })
928
- .then((lst) =>
929
- Promise.all(
930
- lst.map((it) =>
931
- it.isDirectory()
932
- ? rmdir(ospath.join(dir, it.name))
933
- : fsp.unlink(ospath.join(dir, it.name)).catch((unlinkErr) => {
934
- if (unlinkErr.code !== 'ENOENT') throw unlinkErr
935
- })
936
- )
937
- )
938
- )
939
- .then(() => fsp.rmdir(dir))
940
- .catch((err) => {
941
- if (err.code === 'ENOENT') return
942
- if (err.code === 'ENOTDIR') {
943
- return fsp.unlink(dir).catch((unlinkErr) => {
944
- if (unlinkErr.code !== 'ENOENT') throw unlinkErr
945
- })
946
- }
947
- throw err
907
+ function symlinkAwareStat (path_) {
908
+ return fsp.lstat(path_).then((lstat) => {
909
+ if (!lstat.isSymbolicLink()) return lstat
910
+ return fsp.stat(path_).catch((statErr) => {
911
+ throw Object.assign(statErr, { symlink: true })
948
912
  })
913
+ })
949
914
  }
950
915
 
951
916
  function tagsSpecified (sources) {
@@ -1027,6 +992,10 @@ function transformGitCloneError (err, displayUrl) {
1027
992
  return wrappedErr
1028
993
  }
1029
994
 
995
+ function splitRefPatterns (str) {
996
+ return ~str.indexOf('{') ? str.split(VENTILATED_CSV_RX) : str.split(CSV_RX)
997
+ }
998
+
1030
999
  function coerceToString (value) {
1031
1000
  return value == null ? '' : String(value)
1032
1001
  }
@@ -1039,10 +1008,11 @@ function findWorktrees (repo, patterns) {
1039
1008
  if (!patterns.length) return new Map()
1040
1009
  const linkedOnly = patterns[0] === '.' ? !(patterns = patterns.slice(1)) : true
1041
1010
  let worktreesDir
1011
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
1042
1012
  return (patterns.length
1043
1013
  ? fsp
1044
1014
  .readdir((worktreesDir = ospath.join(repo.dir, '.git', 'worktrees')))
1045
- .then((worktreeNames) => matcher(worktreeNames, [...patterns]), invariably.emptyArray)
1015
+ .then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
1046
1016
  .then((worktreeNames) =>
1047
1017
  worktreeNames.length
1048
1018
  ? Promise.all(
package/lib/constants.js CHANGED
@@ -4,23 +4,12 @@ module.exports = Object.freeze({
4
4
  COMPONENT_DESC_FILENAME: 'antora.yml',
5
5
  CONTENT_CACHE_FOLDER: 'content',
6
6
  CONTENT_SRC_GLOB: '**/*[!~]',
7
- CONTENT_SRC_OPTS: { follow: true, nomount: true, nosort: true, nounique: true, removeBOM: false, uniqueBy: (m) => m },
7
+ CONTENT_SRC_OPTS: { follow: true, nomount: true, nosort: true, nounique: true, strict: false, uniqueBy: (m) => m },
8
8
  FILE_MODES: { 100644: 0o100666 & ~process.umask(), 100755: 0o100777 & ~process.umask() },
9
9
  GIT_CORE: 'antora',
10
10
  GIT_OPERATION_LABEL_LENGTH: 8,
11
11
  GIT_PROGRESS_PHASES: ['Counting objects', 'Compressing objects', 'Receiving objects', 'Resolving deltas'],
12
- PICOMATCH_VERSION_OPTS: {
13
- bash: true,
14
- debug: false,
15
- dot: true,
16
- fastpaths: false,
17
- lookbehinds: false,
18
- noextglob: true,
19
- noglobstar: true,
20
- nonegate: true,
21
- noquantifiers: true,
22
- strictSlashes: true,
23
- },
12
+ REF_PATTERN_CACHE_KEY: Symbol('RefPatternCache'),
24
13
  SYMLINK_FILE_MODE: '120000',
25
14
  VALID_STATE_FILENAME: 'valid',
26
15
  })
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ const { makeMatcherRx, refMatcherOpts: getMatcherOpts, MATCH_ALL_RX } = require('./matcher')
4
+
5
+ function compileRx (pattern, opts) {
6
+ if (pattern === '*' || pattern === '**') return MATCH_ALL_RX
7
+ return pattern.charAt() === '!' // do our own negate
8
+ ? Object.defineProperty(makeMatcherRx(pattern.substr(1), opts), 'negated', { value: true })
9
+ : makeMatcherRx(pattern, opts)
10
+ }
11
+
12
+ function createMatcher (patterns, cache, opts) {
13
+ const rxs = patterns.map(
14
+ (pattern) =>
15
+ cache.get(pattern) || cache.set(pattern, compileRx(pattern, opts || (opts = getMatcherOpts(cache)))).get(pattern)
16
+ )
17
+ if (rxs[0].negated) rxs.unshift(MATCH_ALL_RX)
18
+ return (candidate) => {
19
+ let matched
20
+ for (const rx of rxs) {
21
+ let voteIfMatched = true
22
+ if (matched) {
23
+ if (!rx.negated) continue
24
+ voteIfMatched = false
25
+ } else if (rx.negated) {
26
+ continue
27
+ }
28
+ if (rx.test(candidate)) matched = voteIfMatched
29
+ }
30
+ return matched
31
+ }
32
+ }
33
+
34
+ function filterRefs (candidates, patterns, cache = Object.assign(new Map(), { braces: new Map() })) {
35
+ const isMatch = createMatcher(patterns, cache)
36
+ return candidates.reduce((accum, candidate) => {
37
+ if (isMatch(candidate)) accum.push(candidate)
38
+ return accum
39
+ }, [])
40
+ }
41
+
42
+ module.exports = filterRefs
@@ -2,17 +2,16 @@
2
2
 
3
3
  const { homedir } = require('os')
4
4
  const expandPath = require('@antora/expand-path-helper')
5
- const invariably = { void: () => undefined }
6
5
  const { promises: fsp } = require('fs')
6
+ const invariably = require('./invariably')
7
7
  const ospath = require('path')
8
8
 
9
9
  class GitCredentialManagerStore {
10
10
  configure ({ config, startDir }) {
11
11
  this.entries = undefined
12
+ this.path = undefined
12
13
  this.urls = {}
13
- if ((this.contents = (config = config || {}).contents) || !config.path) {
14
- this.path = undefined
15
- } else {
14
+ if (!(this.contents = (config = config || {}).contents) && config.path) {
16
15
  this.path = expandPath(config.path, { dot: startDir })
17
16
  }
18
17
  return this
@@ -35,14 +34,13 @@ class GitCredentialManagerStore {
35
34
  'git',
36
35
  'credentials'
37
36
  )
38
- contentsPromise = fsp
39
- .access(homeGitCredentialsPath)
40
- .then(() => fsp.readFile(homeGitCredentialsPath, 'utf8'))
41
- .catch(() =>
37
+ contentsPromise = fsp.access(homeGitCredentialsPath).then(
38
+ () => fsp.readFile(homeGitCredentialsPath, 'utf8'),
39
+ () =>
42
40
  fsp
43
41
  .access(xdgConfigGitCredentialsPath)
44
42
  .then(() => fsp.readFile(xdgConfigGitCredentialsPath, 'utf8'), invariably.void)
45
- )
43
+ )
46
44
  }
47
45
  contentsPromise.then((contents) => {
48
46
  if (contents) {
package/lib/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Responsible for aggregating the content from multiple repositories and
7
7
  * references into a raw aggregate of virtual files that can be organized by a
8
- * subsequent step in the pipeline.
8
+ * subsequent step in the generator.
9
9
  *
10
10
  * @namespace content-aggregator
11
11
  */
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
package/lib/matcher.js ADDED
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const { compile: bracesToGroup, expand: expandBraces } = require('braces')
4
+ const { makeRe: makeMatcherRx } = require('picomatch')
5
+
6
+ const BASE_OPTS = {
7
+ bash: true,
8
+ dot: true,
9
+ expandRange: (begin, end, step, opts) => bracesToGroup(opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`),
10
+ fastpaths: false,
11
+ nobracket: true,
12
+ noglobstar: true,
13
+ nonegate: true,
14
+ noquantifiers: true,
15
+ regex: false,
16
+ strictSlashes: true,
17
+ }
18
+
19
+ module.exports = {
20
+ MATCH_ALL_RX: { test: () => true },
21
+ expandBraces,
22
+ makeMatcherRx,
23
+ pathMatcherOpts: Object.assign({}, BASE_OPTS, { dot: false }),
24
+ refMatcherOpts: (cache) =>
25
+ Object.assign({}, BASE_OPTS, {
26
+ expandRange: (begin, end, step, opts) => {
27
+ const pattern = opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`
28
+ return cache.braces.get(pattern) || cache.braces.set(pattern, bracesToGroup(pattern)).get(pattern)
29
+ },
30
+ }),
31
+ versionMatcherOpts: Object.assign({}, BASE_OPTS, { nonegate: false }),
32
+ }
@@ -1,26 +1,21 @@
1
1
  'use strict'
2
2
 
3
- const { expand: expandBraces } = require('braces')
4
3
  const flattenDeep = require('./flatten-deep')
5
4
  const { promises: fsp } = require('fs')
6
5
  const git = require('./git')
7
- const invariably = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
8
- const { makeRe: makePicomatchRx } = require('picomatch')
6
+ const invariably = require('./invariably')
7
+ const { expandBraces, makeMatcherRx, pathMatcherOpts: MATCHER_OPTS } = require('./matcher')
9
8
 
10
- const RX_ESCAPE_EXCEPT_GLOB = /[.+?^${}()|[\]\\]/g
11
- const RX_MAGIC_DETECTOR = /[*{]/
12
- const RX_QUESTION_MARK = /\?/g
13
- const PICOMATCH_OPTS = { nobracket: true, noextglob: true, noglobstar: true, noquantifiers: true }
14
- const PICOMATCH_NEGATED_OPTS = { nobracket: true, noextglob: true, noquantifiers: true }
9
+ const NON_GLOB_SPECIAL_CHARS_RX = /[.+?^${}()|[\]\\]/g
10
+ const RX_MAGIC_DETECTOR = /[*{(]/
15
11
 
16
12
  function resolvePathGlobs (base, patterns, listDirents, retrievePath, tree = { path: '' }) {
17
13
  return patterns.reduce((paths, pattern) => {
18
14
  if (pattern.charAt() === '!') {
19
15
  return paths.then((resolvedPaths) => {
20
16
  if (resolvedPaths.length) {
21
- if (~pattern.indexOf('?')) pattern = pattern.replace(RX_QUESTION_MARK, '\\?')
22
- const rx = makePicomatchRx(pattern, PICOMATCH_NEGATED_OPTS)
23
- return resolvedPaths.filter(rx.test.bind(rx))
17
+ const rx = makeMatcherRx(pattern.substr(1), MATCHER_OPTS)
18
+ return resolvedPaths.filter((it) => !rx.test(it))
24
19
  } else {
25
20
  return resolvedPaths
26
21
  }
@@ -38,15 +33,14 @@ async function glob (base, patternSegments, listDirents, retrievePath, { oid, pa
38
33
  let patternSegment = patternSegments[0]
39
34
  patternSegments = patternSegments.slice(1)
40
35
  if (RX_MAGIC_DETECTOR.test(patternSegment)) {
41
- let isMatch
42
- let explicit
36
+ let isMatch, explicit
43
37
  if (patternSegment === '*') {
44
38
  isMatch = (it) => it.charAt() !== '.'
39
+ } else if (~patternSegment.indexOf('(')) {
40
+ isMatch = (isMatch = makeMatcherRx(patternSegment, MATCHER_OPTS)).test.bind(isMatch)
45
41
  } else if (~patternSegment.indexOf('{')) {
46
42
  if (globbed) {
47
- if (patternSegment.charAt() === '!') patternSegment = '\\' + patternSegment
48
- if (~patternSegment.indexOf('?')) patternSegment = patternSegment.replace(RX_QUESTION_MARK, '\\?')
49
- isMatch = (isMatch = makePicomatchRx(patternSegment, PICOMATCH_OPTS)).test.bind(isMatch)
43
+ isMatch = (isMatch = makeMatcherRx(patternSegment, MATCHER_OPTS)).test.bind(isMatch)
50
44
  } else if (~patternSegment.indexOf('*')) {
51
45
  const [wildPatterns, literals] = expandBraces(patternSegment).reduce(
52
46
  ([wild, literal], it) => (~it.indexOf('*') ? [[...wild, it], literal] : [wild, [...literal, it]]),
@@ -58,7 +52,7 @@ async function glob (base, patternSegments, listDirents, retrievePath, { oid, pa
58
52
  return expandBraces(patternSegment).map((it) => joinPath(path, it))
59
53
  }
60
54
  } else {
61
- isMatch = (isMatch = makeMatcherRx(patternSegment)).test.bind(isMatch)
55
+ isMatch = (isMatch = makeSingleMatcherRx(patternSegment)).test.bind(isMatch)
62
56
  }
63
57
  let dirents = await listDirents(base, oid || path)
64
58
  if (explicit) dirents = dirents.filter((dirent) => !explicit.has(dirent.name))
@@ -90,22 +84,15 @@ async function glob (base, patternSegments, listDirents, retrievePath, { oid, pa
90
84
  })
91
85
  } else if ((patternSegment += '/' + patternSegments.join('/')).indexOf('{')) {
92
86
  return expandBraces(patternSegment).map((it) => joinPath(path, it))
93
- } else {
94
- return [joinPath(path, patternSegment)]
95
87
  }
88
+ return [joinPath(path, patternSegment)]
96
89
  } else if (globbed) {
97
90
  return (await retrievePath(base, { oid, path }, patternSegment)) ? [joinPath(path, patternSegment)] : []
98
- } else {
99
- return [joinPath(path, patternSegment)]
100
91
  }
92
+ return [joinPath(path, patternSegment)]
101
93
  }
102
94
  }
103
95
 
104
- function regexpEscapeWithGlob (str) {
105
- // we don't escape "-" since it's meaningless in a literal
106
- return str.replace(RX_ESCAPE_EXCEPT_GLOB, '\\$&').replace('*', '.*')
107
- }
108
-
109
96
  function extractMagicBase (patternSegments, base) {
110
97
  let nextSegment
111
98
  if (patternSegments.length) {
@@ -135,12 +122,18 @@ function makeAlternationMatcherRx (patterns) {
135
122
  return new RegExp('^(?:' + patterns.map(patternToRx).join('|') + ')$')
136
123
  }
137
124
 
138
- function makeMatcherRx (pattern) {
125
+ function makeSingleMatcherRx (pattern) {
139
126
  return new RegExp('^' + patternToRx(pattern) + '$')
140
127
  }
141
128
 
142
129
  function patternToRx (pattern) {
143
- return (pattern.charAt() === '.' ? '' : '(?!\\.)') + regexpEscapeWithGlob(pattern)
130
+ return (
131
+ (pattern.charAt() === '.' ? '' : '(?!\\.)') +
132
+ pattern
133
+ .replace(NON_GLOB_SPECIAL_CHARS_RX, '\\$&')
134
+ .replace('\\\\*', '\\x2a')
135
+ .replace('*', '.*?')
136
+ )
144
137
  }
145
138
 
146
139
  function readdirWithFileTypes (dir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/content-aggregator",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.0-beta.4",
4
4
  "description": "Fetches and aggregates content from distributed sources for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -21,21 +21,23 @@
21
21
  "@antora/user-require-helper": "~2.0",
22
22
  "braces": "~3.0",
23
23
  "cache-directory": "~2.0",
24
- "camelcase-keys": "~6.2",
24
+ "camelcase-keys": "~7.0",
25
+ "glob-stream": "~7.0",
25
26
  "hpagent": "~0.1.0",
26
27
  "isomorphic-git": "~1.10",
27
28
  "js-yaml": "~4.1",
28
- "matcher": "~4.0",
29
29
  "multi-progress": "~4.0",
30
30
  "picomatch": "~2.3",
31
31
  "progress": "~2.0",
32
32
  "should-proxy": "~1.0",
33
33
  "simple-get": "~4.0",
34
- "vinyl": "~2.2",
35
- "vinyl-fs": "~3.0"
34
+ "vinyl": "~2.2"
35
+ },
36
+ "devDependencies": {
37
+ "node-git-server": "~0.6"
36
38
  },
37
39
  "engines": {
38
- "node": ">=10.17.0"
40
+ "node": ">=12.21.0"
39
41
  },
40
42
  "files": [
41
43
  "lib/"
@@ -50,5 +52,6 @@
50
52
  "static site",
51
53
  "web publishing"
52
54
  ],
53
- "gitHead": "a504d6889819b548e8a5416a7194cbb6f9a93e93"
55
+ "gitHead": "8a142499e9f1a9e0631777796e06dd6c010d3a90",
56
+ "readmeFilename": "README.md"
54
57
  }
@@ -1,11 +0,0 @@
1
- 'use strict'
2
-
3
- Promise.allSettled = (iterable) =>
4
- Promise.all(
5
- iterable.map((it) =>
6
- it.then(
7
- (value) => ({ status: 'fulfilled', value }),
8
- (reason) => ({ status: 'rejected', reason })
9
- )
10
- )
11
- )