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

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
 
@@ -9,6 +9,7 @@ const decodeUint8Array = require('./decode-uint8-array')
9
9
  const EventEmitter = require('events')
10
10
  const expandPath = require('@antora/expand-path-helper')
11
11
  const File = require('./file')
12
+ const filterRefs = require('./filter-refs')
12
13
  const flattenDeep = require('./flatten-deep')
13
14
  const fs = require('fs')
14
15
  const { promises: fsp } = fs
@@ -18,7 +19,6 @@ const git = require('./git')
18
19
  const { NotFoundError, ObjectTypeError, UnknownTransportError, UrlParseError } = git.Errors
19
20
  const invariably = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
20
21
  const { makeRe: makePicomatchRx } = require('picomatch')
21
- const matcher = require('matcher')
22
22
  const MultiProgress = require('multi-progress')
23
23
  const ospath = require('path')
24
24
  const { posix: path } = ospath
@@ -40,6 +40,7 @@ const {
40
40
  GIT_OPERATION_LABEL_LENGTH,
41
41
  GIT_PROGRESS_PHASES,
42
42
  PICOMATCH_VERSION_OPTS,
43
+ REF_PATTERN_CACHE_KEY,
43
44
  SYMLINK_FILE_MODE,
44
45
  VALID_STATE_FILENAME,
45
46
  } = require('./constants')
@@ -101,7 +102,8 @@ function aggregateContent (playbook) {
101
102
  return accum.set(source.url, [...(accum.get(source.url) || []), Object.assign({}, sourceDefaults, source)])
102
103
  }, new Map())
103
104
  const progress = !quiet && createProgress(sourcesByUrl.keys(), process.stdout)
104
- const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir }
105
+ const refPatternCache = Object.assign(new Map(), { braces: new Map() })
106
+ const loadOpts = { cacheDir, fetch, gitPlugins, progress, startDir, refPatternCache }
105
107
  return collectFiles(sourcesByUrl, loadOpts, fetchConcurrency).then(buildAggregate, (err) => {
106
108
  progress && progress.terminate()
107
109
  throw err
@@ -173,13 +175,14 @@ function buildAggregate (componentVersionBuckets) {
173
175
 
174
176
  async function loadRepository (url, opts) {
175
177
  let authStatus, dir, repo
178
+ const cache = { [REF_PATTERN_CACHE_KEY]: opts.refPatternCache }
176
179
  if (~url.indexOf(':') && GIT_URI_DETECTOR_RX.test(url)) {
177
180
  let credentials, displayUrl
178
181
  ;({ displayUrl, url, credentials } = extractCredentials(url))
179
182
  const { cacheDir, fetch, fetchTags, gitPlugins, progress } = opts
180
183
  dir = ospath.join(cacheDir, generateCloneFolderName(displayUrl))
181
184
  // 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 }
185
+ repo = { cache, dir, fs, gitdir: dir, noCheckout: true, url }
183
186
  const validStateFile = ospath.join(dir, VALID_STATE_FILENAME)
184
187
  try {
185
188
  await fsp.access(validStateFile)
@@ -204,7 +207,7 @@ async function loadRepository (url, opts) {
204
207
  authStatus = await git.getConfig(Object.assign({ path: 'remote.origin.private' }, repo))
205
208
  }
206
209
  } catch (gitErr) {
207
- await rmdir(dir)
210
+ await fsp['rm' in fsp ? 'rm' : 'rmdir'](dir, { recursive: true, force: true })
208
211
  if (gitErr.rethrow) throw transformGitCloneError(gitErr, displayUrl)
209
212
  const fetchOpts = buildFetchOptions(repo, progress, displayUrl, credentials, gitPlugins, fetchTags, 'clone')
210
213
  await git
@@ -225,9 +228,7 @@ async function loadRepository (url, opts) {
225
228
  }
226
229
  } else if (await isDirectory((dir = expandPath(url, { dot: opts.startDir })))) {
227
230
  const gitdir = ospath.join(dir, '.git')
228
- repo = (await isDirectory(gitdir))
229
- ? { cache: {}, dir, fs, gitdir }
230
- : { cache: {}, dir, fs, gitdir: dir, noCheckout: true }
231
+ repo = (await isDirectory(gitdir)) ? { cache, dir, fs, gitdir } : { cache, dir, fs, gitdir: dir, noCheckout: true }
231
232
  try {
232
233
  await git.resolveRef(Object.assign({ ref: 'HEAD', depth: 1 }, repo))
233
234
  } catch {
@@ -271,15 +272,16 @@ async function collectFilesFromSource (source, repo, remoteName, authStatus) {
271
272
  async function selectReferences (source, repo, remote) {
272
273
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns = '.' } = source
273
274
  const isBare = repo.noCheckout
275
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
274
276
  const noWorktree = repo.url ? undefined : null
275
277
  const refs = new Map()
276
278
  if (tagPatterns) {
277
279
  tagPatterns = Array.isArray(tagPatterns)
278
280
  ? tagPatterns.map((pattern) => String(pattern))
279
- : String(tagPatterns).split(CSV_RX)
281
+ : splitRefPatterns(String(tagPatterns))
280
282
  if (tagPatterns.length) {
281
283
  const tags = await git.listTags(repo)
282
- for (const shortname of tags.length ? matcher(tags, tagPatterns) : tags) {
284
+ for (const shortname of tags.length ? filterRefs(tags, tagPatterns, patternCache) : tags) {
283
285
  // NOTE tags are stored using symbol keys to distinguish them from branches
284
286
  refs.set(Symbol(shortname), { shortname, fullname: 'tags/' + shortname, type: 'tag', head: noWorktree })
285
287
  }
@@ -294,7 +296,7 @@ async function selectReferences (source, repo, remote) {
294
296
  } else {
295
297
  worktreePatterns = Array.isArray(worktreePatterns)
296
298
  ? worktreePatterns.map((pattern) => String(pattern))
297
- : String(worktreePatterns).split(CSV_RX)
299
+ : splitRefPatterns(String(worktreePatterns))
298
300
  }
299
301
  }
300
302
  const branchPatternsString = String(branchPatterns)
@@ -313,7 +315,7 @@ async function selectReferences (source, repo, remote) {
313
315
  } else if (
314
316
  (branchPatterns = Array.isArray(branchPatterns)
315
317
  ? branchPatterns.map((pattern) => String(pattern))
316
- : branchPatternsString.split(CSV_RX)).length
318
+ : splitRefPatterns(branchPatternsString)).length
317
319
  ) {
318
320
  let headBranchIdx
319
321
  // NOTE we can assume at least two entries if HEAD or . are present
@@ -345,7 +347,7 @@ async function selectReferences (source, repo, remote) {
345
347
  // NOTE isomorphic-git includes HEAD in list of remote branches (see https://isomorphic-git.org/docs/listBranches)
346
348
  const remoteBranches = (await git.listBranches(Object.assign({ remote }, repo))).filter((it) => it !== 'HEAD')
347
349
  if (remoteBranches.length) {
348
- for (const shortname of matcher(remoteBranches, branchPatterns)) {
350
+ for (const shortname of filterRefs(remoteBranches, branchPatterns, patternCache)) {
349
351
  const fullname = 'remotes/' + remote + '/' + shortname
350
352
  refs.set(shortname, { shortname, fullname, type: 'branch', remote, head: noWorktree })
351
353
  }
@@ -355,16 +357,17 @@ async function selectReferences (source, repo, remote) {
355
357
  const localBranches = await git.listBranches(repo)
356
358
  if (localBranches.length) {
357
359
  const worktrees = await findWorktrees(repo, worktreePatterns)
358
- for (const shortname of matcher(localBranches, branchPatterns)) {
360
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
359
361
  const head = worktrees.get(shortname) || noWorktree
360
362
  refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head })
361
363
  }
362
364
  }
363
365
  } else if (!remoteBranches.length) {
364
- // QUESTION should local branches be used if the only remote branch is HEAD?
365
366
  const localBranches = await git.listBranches(repo)
366
- for (const shortname of localBranches.length ? matcher(localBranches, branchPatterns) : localBranches) {
367
- refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
367
+ if (localBranches.length) {
368
+ for (const shortname of filterRefs(localBranches, branchPatterns, patternCache)) {
369
+ refs.set(shortname, { shortname, fullname: 'heads/' + shortname, type: 'branch', head: noWorktree })
370
+ }
368
371
  }
369
372
  }
370
373
  }
@@ -601,7 +604,7 @@ function readGitSymlink (repo, root, parent, { oid }, following) {
601
604
  let targetParent
602
605
  if (parent.dirname) {
603
606
  const dirname = parent.dirname + '/'
604
- target = path.join(dirname, target)
607
+ target = path.join(dirname, target) // join doesn't remove trailing separator
605
608
  if (target.startsWith(dirname)) {
606
609
  target = target.substr(dirname.length)
607
610
  targetParent = parent
@@ -609,10 +612,12 @@ function readGitSymlink (repo, root, parent, { oid }, following) {
609
612
  targetParent = root
610
613
  }
611
614
  } else {
612
- target = path.normalize(target)
615
+ target = path.normalize(target) // normalize doesn't remove trailing separator
613
616
  targetParent = root
614
617
  }
615
- return readGitObjectAtPath(repo, root, targetParent, target.split('/'), following)
618
+ const targetSegments = target.split('/')
619
+ if (!targetSegments[targetSegments.length - 1]) targetSegments.pop()
620
+ return readGitObjectAtPath(repo, root, targetParent, targetSegments, following)
616
621
  })
617
622
  }
618
623
  const err = { name: 'SymbolicLinkCycleError', code: 'SymbolicLinkCycleError', oid }
@@ -918,36 +923,6 @@ function isDirectory (url) {
918
923
  return fsp.stat(url).then((stat) => stat.isDirectory(), invariably.false)
919
924
  }
920
925
 
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
948
- })
949
- }
950
-
951
926
  function tagsSpecified (sources) {
952
927
  return sources.some(({ tags }) => tags && (Array.isArray(tags) ? tags.length : true))
953
928
  }
@@ -1027,6 +1002,10 @@ function transformGitCloneError (err, displayUrl) {
1027
1002
  return wrappedErr
1028
1003
  }
1029
1004
 
1005
+ function splitRefPatterns (str) {
1006
+ return ~str.indexOf('{') ? str.split(VENTILATED_CSV_RX) : str.split(CSV_RX)
1007
+ }
1008
+
1030
1009
  function coerceToString (value) {
1031
1010
  return value == null ? '' : String(value)
1032
1011
  }
@@ -1039,10 +1018,11 @@ function findWorktrees (repo, patterns) {
1039
1018
  if (!patterns.length) return new Map()
1040
1019
  const linkedOnly = patterns[0] === '.' ? !(patterns = patterns.slice(1)) : true
1041
1020
  let worktreesDir
1021
+ const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
1042
1022
  return (patterns.length
1043
1023
  ? fsp
1044
1024
  .readdir((worktreesDir = ospath.join(repo.dir, '.git', 'worktrees')))
1045
- .then((worktreeNames) => matcher(worktreeNames, [...patterns]), invariably.emptyArray)
1025
+ .then((worktreeNames) => filterRefs(worktreeNames, [...patterns], patternCache), invariably.emptyArray)
1046
1026
  .then((worktreeNames) =>
1047
1027
  worktreeNames.length
1048
1028
  ? Promise.all(
package/lib/constants.js CHANGED
@@ -11,16 +11,16 @@ module.exports = Object.freeze({
11
11
  GIT_PROGRESS_PHASES: ['Counting objects', 'Compressing objects', 'Receiving objects', 'Resolving deltas'],
12
12
  PICOMATCH_VERSION_OPTS: {
13
13
  bash: true,
14
- debug: false,
15
14
  dot: true,
16
15
  fastpaths: false,
17
- lookbehinds: false,
18
- noextglob: true,
16
+ nobracket: true,
19
17
  noglobstar: true,
20
18
  nonegate: true,
21
19
  noquantifiers: true,
20
+ regex: false,
22
21
  strictSlashes: true,
23
22
  },
23
+ REF_PATTERN_CACHE_KEY: Symbol('RefPatternCache'),
24
24
  SYMLINK_FILE_MODE: '120000',
25
25
  VALID_STATE_FILENAME: 'valid',
26
26
  })
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const { compile: bracesToGroup } = require('braces')
4
+ const { makeRe: makePicomatchRx } = require('picomatch')
5
+
6
+ function getPicomatchOpts (cache) {
7
+ return {
8
+ bash: true,
9
+ dot: true,
10
+ expandRange: (begin, end, step, opts) => {
11
+ const pattern = opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`
12
+ return cache.braces.get(pattern) || cache.braces.set(pattern, bracesToGroup(pattern)).get(pattern)
13
+ },
14
+ fastpaths: false,
15
+ nobracket: true,
16
+ noglobstar: true,
17
+ noquantifiers: true,
18
+ regex: false,
19
+ strictSlashes: true,
20
+ }
21
+ }
22
+
23
+ function compileRx (pattern, opts) {
24
+ if (pattern === '*' || pattern === '**') return { test: () => true }
25
+ return pattern.charAt() === '!' // do our own negate
26
+ ? Object.defineProperty(makePicomatchRx(pattern.substr(1), opts), 'negated', { value: true })
27
+ : makePicomatchRx(pattern, opts)
28
+ }
29
+
30
+ function createMatcher (patterns, cache) {
31
+ let opts
32
+ const rxs = patterns.map(
33
+ (pattern) =>
34
+ cache.get(pattern) ||
35
+ cache.set(pattern, compileRx(pattern, opts || (opts = getPicomatchOpts(cache)))).get(pattern)
36
+ )
37
+ return (candidate) => {
38
+ let first = true
39
+ let matched
40
+ for (const rx of rxs) {
41
+ if (matched) {
42
+ if (rx.negated && rx.test(candidate)) return
43
+ } else if (first || !rx.negated) {
44
+ matched = rx.test(candidate)
45
+ }
46
+ first = false
47
+ }
48
+ return matched
49
+ }
50
+ }
51
+
52
+ function filterRefs (candidates, patterns, cache = Object.assign(new Map(), { braces: new Map() })) {
53
+ const isMatch = createMatcher(patterns, cache)
54
+ return candidates.reduce((accum, candidate) => {
55
+ if (isMatch(candidate)) accum.push(candidate)
56
+ return accum
57
+ }, [])
58
+ }
59
+
60
+ module.exports = filterRefs
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
  */
@@ -1,26 +1,33 @@
1
1
  'use strict'
2
2
 
3
- const { expand: expandBraces } = require('braces')
3
+ const { expand: expandBraces, compile: bracesToGroup } = require('braces')
4
4
  const flattenDeep = require('./flatten-deep')
5
5
  const { promises: fsp } = require('fs')
6
6
  const git = require('./git')
7
7
  const invariably = { true: () => true, false: () => false, void: () => undefined, emptyArray: () => [] }
8
8
  const { makeRe: makePicomatchRx } = require('picomatch')
9
9
 
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 }
10
+ const NON_GLOB_SPECIAL_CHARS_RX = /[.+?^${}()|[\]\\]/g
11
+ const RX_MAGIC_DETECTOR = /[*{(]/
12
+ const PICOMATCH_OPTS = {
13
+ bash: true,
14
+ expandRange: (begin, end, step, opts) => bracesToGroup(opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`),
15
+ fastpaths: false,
16
+ nobracket: true,
17
+ noglobstar: true,
18
+ nonegate: true,
19
+ noquantifiers: true,
20
+ regex: false,
21
+ strictSlashes: true,
22
+ }
15
23
 
16
24
  function resolvePathGlobs (base, patterns, listDirents, retrievePath, tree = { path: '' }) {
17
25
  return patterns.reduce((paths, pattern) => {
18
26
  if (pattern.charAt() === '!') {
19
27
  return paths.then((resolvedPaths) => {
20
28
  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))
29
+ const rx = makePicomatchRx(pattern.substr(1), PICOMATCH_OPTS)
30
+ return resolvedPaths.filter((it) => !rx.test(it))
24
31
  } else {
25
32
  return resolvedPaths
26
33
  }
@@ -38,14 +45,13 @@ async function glob (base, patternSegments, listDirents, retrievePath, { oid, pa
38
45
  let patternSegment = patternSegments[0]
39
46
  patternSegments = patternSegments.slice(1)
40
47
  if (RX_MAGIC_DETECTOR.test(patternSegment)) {
41
- let isMatch
42
- let explicit
48
+ let isMatch, explicit
43
49
  if (patternSegment === '*') {
44
50
  isMatch = (it) => it.charAt() !== '.'
51
+ } else if (~patternSegment.indexOf('(')) {
52
+ isMatch = (isMatch = makePicomatchRx(patternSegment, PICOMATCH_OPTS)).test.bind(isMatch)
45
53
  } else if (~patternSegment.indexOf('{')) {
46
54
  if (globbed) {
47
- if (patternSegment.charAt() === '!') patternSegment = '\\' + patternSegment
48
- if (~patternSegment.indexOf('?')) patternSegment = patternSegment.replace(RX_QUESTION_MARK, '\\?')
49
55
  isMatch = (isMatch = makePicomatchRx(patternSegment, PICOMATCH_OPTS)).test.bind(isMatch)
50
56
  } else if (~patternSegment.indexOf('*')) {
51
57
  const [wildPatterns, literals] = expandBraces(patternSegment).reduce(
@@ -90,22 +96,15 @@ async function glob (base, patternSegments, listDirents, retrievePath, { oid, pa
90
96
  })
91
97
  } else if ((patternSegment += '/' + patternSegments.join('/')).indexOf('{')) {
92
98
  return expandBraces(patternSegment).map((it) => joinPath(path, it))
93
- } else {
94
- return [joinPath(path, patternSegment)]
95
99
  }
100
+ return [joinPath(path, patternSegment)]
96
101
  } else if (globbed) {
97
102
  return (await retrievePath(base, { oid, path }, patternSegment)) ? [joinPath(path, patternSegment)] : []
98
- } else {
99
- return [joinPath(path, patternSegment)]
100
103
  }
104
+ return [joinPath(path, patternSegment)]
101
105
  }
102
106
  }
103
107
 
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
108
  function extractMagicBase (patternSegments, base) {
110
109
  let nextSegment
111
110
  if (patternSegments.length) {
@@ -140,7 +139,13 @@ function makeMatcherRx (pattern) {
140
139
  }
141
140
 
142
141
  function patternToRx (pattern) {
143
- return (pattern.charAt() === '.' ? '' : '(?!\\.)') + regexpEscapeWithGlob(pattern)
142
+ return (
143
+ (pattern.charAt() === '.' ? '' : '(?!\\.)') +
144
+ pattern
145
+ .replace(NON_GLOB_SPECIAL_CHARS_RX, '\\$&')
146
+ .replace('\\\\*', '\\x2a')
147
+ .replace('*', '.*?')
148
+ )
144
149
  }
145
150
 
146
151
  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.1",
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,11 +21,10 @@
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
25
  "hpagent": "~0.1.0",
26
26
  "isomorphic-git": "~1.10",
27
27
  "js-yaml": "~4.1",
28
- "matcher": "~4.0",
29
28
  "multi-progress": "~4.0",
30
29
  "picomatch": "~2.3",
31
30
  "progress": "~2.0",
@@ -35,7 +34,7 @@
35
34
  "vinyl-fs": "~3.0"
36
35
  },
37
36
  "engines": {
38
- "node": ">=10.17.0"
37
+ "node": ">=12.21.0"
39
38
  },
40
39
  "files": [
41
40
  "lib/"
@@ -50,5 +49,5 @@
50
49
  "static site",
51
50
  "web publishing"
52
51
  ],
53
- "gitHead": "a504d6889819b548e8a5416a7194cbb6f9a93e93"
52
+ "gitHead": "7c5ef1ea93dd489af533c80a936c736013c41769"
54
53
  }