@antora/assembler 1.0.0-beta.9 → 1.0.0-rc.2

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.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ autoload :JSON, 'json'
4
+
5
+ class JsonlFormatter < Asciidoctor::Logger::BasicFormatter
6
+ def call severity, time, progname, msg
7
+ entry = { 'time' => time.utc.to_i, 'level' => severity.downcase, 'name' => progname }
8
+ if ::String === msg
9
+ entry['msg'] = msg
10
+ else
11
+ loc = msg[:source_location]
12
+ entry['file'] = { 'path' => loc.file, 'line' => loc.lineno }
13
+ entry['msg'] = msg[:text]
14
+ end
15
+ (JSON.generate entry) + ?\n
16
+ end
17
+ end
18
+
19
+ proc do
20
+ logger = Asciidoctor::LoggerManager.logger
21
+ logger.formatter = JsonlFormatter.new
22
+ unless (progname = ::File.basename $PROGRAM_NAME).empty?
23
+ logger.progname = progname
24
+ end
25
+ end.call
@@ -1,23 +1,32 @@
1
1
  'use strict'
2
2
 
3
- const computeOut = require('./util/compute-out')
4
3
  const fs = require('node:fs')
5
4
  const { promises: fsp } = fs
6
5
  const LazyReadable = require('./util/lazy-readable')
7
6
  const loadConfig = require('./load-config')
7
+ const logCommand = require('./log-command')
8
8
  const ospath = require('node:path')
9
9
  const { posix: path } = ospath
10
10
  const produceAssemblyFiles = require('./produce-assembly-files')
11
11
  const PromiseQueue = require('./util/promise-queue')
12
- const { stringify: toJSON } = JSON
12
+ const runCommand = require('@antora/run-command-helper')
13
13
 
14
14
  const invariably = { new: () => ({}), void: () => undefined }
15
15
  const PACKAGE_NAME = require('../package.json').name
16
16
  const NEWLINE_RX = /(?:\r?\n)+/
17
17
 
18
18
  async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
19
- const assemblerConfig = await loadConfig(playbook, configSource)
20
- if (!assemblerConfig) return [] // TODO consider removing and doing this another way
19
+ const {
20
+ convert = converter,
21
+ getDefaultCommand,
22
+ extname: targetExtname = '',
23
+ backend: targetBackend = targetExtname.substring(1),
24
+ embedReferenceStyle = 'relative',
25
+ mediaType: targetMediaType,
26
+ loggerName = PACKAGE_NAME,
27
+ } = converter ?? {}
28
+ const assemblerConfig = await loadConfig.call(this, playbook, configSource, '-' + targetBackend)
29
+ if (assemblerConfig.enabled === false) return []
21
30
  const context = isBound(this)
22
31
  ? this
23
32
  : {
@@ -27,22 +36,13 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
27
36
  }
28
37
  const generatorFunctions = context.getFunctions()
29
38
  const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
30
- const {
31
- convert = converter,
32
- getDefaultCommand,
33
- extname: targetExtname = '',
34
- backend: targetBackend = targetExtname.slice(1),
35
- embedReferenceStyle = 'relative',
36
- mediaType: targetMediaType,
37
- loggerName = PACKAGE_NAME,
38
- } = converter ?? {}
39
39
  const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
40
40
  assemblyConfig.embedReferenceStyle = embedReferenceStyle
41
- const { profile = targetBackend } = assemblyConfig
41
+ const profile = (assemblyConfig.profile ??= targetBackend)
42
42
  const intrinsicAttributes = { 'loader-assembler': '' }
43
43
  buildConfig.cwd ??= process.cwd()
44
44
  if (profile) {
45
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
45
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler/${profile}`)
46
46
  intrinsicAttributes[`assembler-profile-${profile}`] = ''
47
47
  intrinsicAttributes['assembler-profile'] = profile
48
48
  if (targetBackend) {
@@ -50,19 +50,19 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
50
50
  intrinsicAttributes['assembler-backend'] = targetBackend
51
51
  }
52
52
  } else {
53
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
53
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler/_')
54
54
  }
55
55
  if (targetExtname) {
56
- const targetFiletype = targetExtname.slice(1)
56
+ const targetFiletype = targetExtname.substring(1)
57
57
  intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
58
58
  intrinsicAttributes['assembler-filetype'] = targetFiletype
59
59
  }
60
- Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
60
+ Object.assign(assemblyConfig.attributes, intrinsicAttributes)
61
61
  const assemblyFiles = produceAssemblyFiles(
62
62
  loadAsciiDoc,
63
63
  contentCatalog,
64
64
  assemblerConfig,
65
- createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
65
+ generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
66
66
  )
67
67
  if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
68
68
  if (buildConfig.command == null && typeof getDefaultCommand === 'function') {
@@ -70,14 +70,15 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
70
70
  }
71
71
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
72
72
  await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
73
+ const helpers = { logCommand: logCommand.bind(context, loggerName), runCommand }
73
74
  const boundConvert = convert.bind(context)
74
75
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
75
76
  .add(
76
77
  assemblyFiles.map((doc) => async () => {
77
78
  const relativeToOutput = embedReferenceStyle === 'output-relative'
78
- const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, buildConfig)
79
+ const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
79
80
  if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
80
- return boundConvert(doc, convertAttributes, buildConfig).then((result) => {
81
+ return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) => {
81
82
  const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
82
83
  return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
83
84
  })
@@ -93,7 +94,7 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
93
94
  return files.map((file) => {
94
95
  const pages = file.assembler.assembled.pages
95
96
  delete file.assembler.assembled
96
- file = contentCatalog.addFile(Object.assign(file, { out: computeOut.call(contentCatalog, file.src) }))
97
+ file = contentCatalog.addFile(file)
97
98
  const extname = file.extname
98
99
  const download = file.assembler.downloadStem + extname
99
100
  if (qualifyExports) {
@@ -103,8 +104,14 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
103
104
  }
104
105
  pages.forEach((fragment, page) => {
105
106
  const assemblerMeta = (page.assembler ??= {})
106
- ;(assemblerMeta.exports ??= []).push({ fragment, file })
107
- const extnameProp = extname.slice(1)
107
+ const exports = (assemblerMeta.exports ??= [])
108
+ const exportEntry = { fragment, file }
109
+ const insertIdx = exports.findIndex(
110
+ ({ file: candidate }) =>
111
+ !(candidate.src.component === page.src.component && candidate.src.version === page.src.version)
112
+ )
113
+ ~insertIdx ? exports.splice(insertIdx, 0, exportEntry) : exports.push(exportEntry)
114
+ const extnameProp = extname.substring(1)
108
115
  if (extnameProp in assemblerMeta) return
109
116
  Object.defineProperty(assemblerMeta, extnameProp, {
110
117
  configurable: true,
@@ -117,85 +124,81 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
117
124
  })
118
125
  }
119
126
 
120
- function createResolveAssemblyModel (context, contentCatalog, common, intrinsicAttributes, navigationCatalog) {
127
+ /**
128
+ * Generates a function that selects the active assembly profile, initializes an assembly model using
129
+ * the keys from the profile as well as any inherited shared keys, builds the navigation for the assembly, and
130
+ * returns the initialized assembly model. The assembly model is further populated after the call to this function.
131
+ */
132
+ function generateSelectAssemblyProfile (context, contentCatalog, baseModel, intrinsicAttributes, navigationCatalog) {
121
133
  const logger = context.getLogger?.(PACKAGE_NAME)
122
134
  const { assemblerProfiles } = context.getVariables()
123
135
  if (!assemblerProfiles) {
124
136
  return (componentVersion) => {
125
137
  const navigation =
126
138
  navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
127
- return Object.assign({ logger }, common, { navigation })
139
+ return Object.assign({}, baseModel, { attributes: Object.assign({}, baseModel.attributes), navigation, logger })
128
140
  }
129
141
  }
130
142
  const boundSendToLog = sendToLog.bind(logger)
131
- const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
143
+ const { buildAlternateNavigation = require('@antora/navigation-builder').buildAlternateNavigation } =
144
+ context.getFunctions()
132
145
  return (componentVersion) => {
133
146
  const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
134
- const overrides =
135
- componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
136
- const { navFiles, messages } = overrides
137
- const model = Object.assign({ logger }, common, overrides)
147
+ const overrides = componentVersionProfiles?.get(baseModel.profile) ?? componentVersionProfiles?.get()
148
+ const navigation = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
149
+ const attributes = Object.assign({}, baseModel.attributes)
150
+ const model = Object.assign({}, baseModel, overrides, { attributes, logger })
151
+ if (!overrides) return Object.assign(model, { navigation: navigation || componentVersion.navigation })
138
152
  delete model.navFiles
139
153
  delete model.messages
140
- const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
141
- if (navigationOverride) return Object.assign(model, { navigation: navigationOverride })
142
- messages?.forEach(boundSendToLog)
143
- model.navigation = componentVersion.navigation
144
- if (!navFiles) return model
154
+ if (overrides.attributes) Object.entries(overrides.attributes).forEach(([name, val]) => (attributes[name] = val))
155
+ if (navigation) return Object.assign(model, { navigation })
156
+ overrides.messages?.forEach(boundSendToLog)
157
+ const navFiles = overrides.navFiles
158
+ if (!navFiles) return Object.assign(model, { navigation: componentVersion.navigation })
145
159
  if (!navFiles.length) return Object.assign(model, { navigation: [] })
146
- const navigation_ = model.navigation
147
- const asciidoc_ = componentVersion.asciidoc
148
- componentVersion.asciidoc = Object.assign({}, asciidoc_, {
149
- attributes: Object.assign({}, asciidoc_.attributes, intrinsicAttributes),
160
+ model.navigation = buildAlternateNavigation(contentCatalog, componentVersion, navFiles, {
161
+ attributes: intrinsicAttributes,
150
162
  })
151
- buildNavigation(
152
- new Proxy(contentCatalog, {
153
- get (target, property) {
154
- const method = target[property]
155
- if (property !== 'findBy') return method
156
- return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria))
157
- },
158
- })
159
- )
160
- model.navigation = componentVersion.navigation
161
- Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ })
162
163
  return model
163
164
  }
164
165
  }
165
166
 
166
- function prepareConvertAttributes (doc, targetExtname, relativeToOutput, buildConfig) {
167
+ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assemblerConfig) {
167
168
  const {
168
169
  asciidoc: { attributes: docAttributes } = { attributes: {} },
169
170
  extname: docfilesuffix,
170
171
  path: reldocfile,
171
172
  src: { family, relative },
172
173
  } = doc
173
- const { cwd = process.cwd(), dir = cwd } = buildConfig
174
- const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
174
+ const { cwd = process.cwd(), dir = cwd } = assemblerConfig.build
175
+ const docname = family + '$' + relative.substring(0, relative.length - docfilesuffix.length)
175
176
  const docfile = ospath.join(dir, reldocfile)
176
177
  const outdir = ospath.dirname(docfile)
177
178
  const docdir = relativeToOutput ? dir : outdir
178
- const imagesdir = ''
179
- const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
180
- const attributes = Object.assign({}, docAttributes, {
179
+ const outfile = docfile.substring(0, docfile.length - docfilesuffix.length) + targetExtname
180
+ const attributes = Object.assign({ revdate: `${assemblerConfig.assembly.revdate}@` }, docAttributes, {
181
181
  docdir,
182
182
  docfile,
183
183
  docfilesuffix,
184
184
  'docname@': docname,
185
- imagesdir,
185
+ imagesdir: '',
186
186
  outdir,
187
187
  outfile,
188
188
  outfilesuffix: targetExtname,
189
- toArgs (optionFlag, command) {
190
- const padCharRef = process.platform === 'win32' && command.startsWith('bundle exec ')
189
+ toArgs (optionFlag) {
191
190
  const args = []
192
191
  for (let [name, val] of Object.entries(this)) {
193
192
  if (val) {
194
- val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}`
193
+ val = name + '=' + val
195
194
  } else if (val === '') {
195
+ if (name === 'asciidoctor-log-integration') {
196
+ args.push('-r', require.resolve('#asciidoctor-log-adapter'))
197
+ continue
198
+ }
196
199
  val = name
197
200
  } else {
198
- val = `!${name}${val === false ? '@' : ''}`
201
+ val = `${name}!${val === false ? '@' : ''}`
199
202
  }
200
203
  args.push(optionFlag, val)
201
204
  }
@@ -212,10 +215,9 @@ function coerceToExportFormat (originalFile, targetBackend, targetExtname, targe
212
215
  : fileOrContents
213
216
  ;(file.assembler ??= {}).backend = targetBackend
214
217
  if (file.extname === targetExtname) return file
215
- const sourcePath = file.path
216
- const sourceExtname = file.extname
217
- const relativeWithoutExtname = file.src.relative.slice(0, file.src.relative.length - sourceExtname.length)
218
- const newPath = sourcePath.slice(0, sourcePath.length - sourceExtname.length) + targetExtname
218
+ const { path: sourcePath, extname: sourceExtname } = file
219
+ const relativeWithoutExtname = file.src.relative.substring(0, file.src.relative.length - sourceExtname.length)
220
+ const newPath = sourcePath.substring(0, sourcePath.length - sourceExtname.length) + targetExtname
219
221
  Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: newPath })
220
222
  file.src.basename = path.basename((file.src.relative = relativeWithoutExtname + (file.src.extname = targetExtname)))
221
223
  return file
@@ -237,14 +239,30 @@ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
237
239
  outPaths.add(asset.out.path)
238
240
  }
239
241
  }
240
- if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } })))
241
- return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
242
+ if (keepSource) {
243
+ for (const file of assemblyFiles) {
244
+ file.src.contents = file.contents
245
+ file.out = { path: file.path }
246
+ files.push(file)
247
+ }
248
+ }
249
+ return publishFiles({ output: { clean, dir } }, { getFiles: () => files }).then(() => {
250
+ if (keepSource) {
251
+ for (const file of assemblyFiles) delete file.out
252
+ }
253
+ })
242
254
  }
243
255
 
244
256
  function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
245
257
  let fileOrContents
246
258
  if (convertResult?.status != null) {
247
- fileOrContents = 'file' in convertResult ? convertResult.file : convertResult.contents
259
+ if ('file' in convertResult) {
260
+ fileOrContents = convertResult.file
261
+ } else if ('contents' in convertResult) {
262
+ fileOrContents = convertResult.contents
263
+ } else if (buildConfig.outputOptionFlag === false) {
264
+ fileOrContents = convertResult.stdout
265
+ }
248
266
  let logger, match
249
267
  if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
250
268
  const docfile = convertAttributes.docfile
@@ -257,10 +275,15 @@ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, l
257
275
  if (entry.name) ctx.program = entry.name
258
276
  if (entry.file?.line) ctx.line = entry.file.line
259
277
  logger[entry.level](ctx, entry.msg)
260
- } else if ((match = /^(.+):(\d+): warning: (.+)/.exec(line))) {
278
+ } else if ((match = /^([^:]+):(\d+): warning: (.+)/.exec(line))) {
261
279
  const [, scriptPath, lineno, msg] = match
262
280
  ctx.stack = [{ file: { path: scriptPath }, line: parseInt(lineno, 10) }]
263
281
  logger.warn(ctx, msg)
282
+ } else if ((match = /^asciidoctor: ([A-Z]+): (?:[^:]+: line (\d+): )?(.+)/.exec(line))) {
283
+ // NOTE we don't care about the filename in the message since it can only be docfile
284
+ const [, level, lineno, msg] = match
285
+ if (lineno) ctx.line = parseInt(lineno, 10)
286
+ logger[level === 'WARNING' ? 'warn' : level.toLowerCase()](ctx, msg)
264
287
  } else {
265
288
  logger.info(ctx, line)
266
289
  }
package/lib/configure.js CHANGED
@@ -1,30 +1,54 @@
1
1
  'use strict'
2
2
 
3
+ const { ASSEMBLY_KEYS } = require('./constants')
4
+
3
5
  function configure (context, ...args) {
4
6
  internalConfigure.apply(context, args)
5
7
  }
6
8
 
7
9
  function internalConfigure (converter, config = {}, providers = {}) {
8
10
  this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
11
+ contentCatalog.publishableFamilies.add('export')
9
12
  if (assemblerProfiles) return
10
13
  this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
11
14
  })
12
15
 
13
- this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
14
- siteAsciiDocConfig.keepSource = true
15
- })
16
+ this.once('beforeProcess', enableKeepSource)
16
17
 
17
18
  this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
18
19
  const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
19
- assembleContentConfig.configSource ??= config.configFile
20
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
20
+ if (assembleContentConfig.configSource?.constructor === Object) {
21
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
22
+ } else {
23
+ const singleConfig = !('configFiles' in config)
24
+ const configFiles = singleConfig ? config.configFile : config.configFiles
25
+ for (const configSource of Array.isArray(configFiles) ? configFiles : [configFiles]) {
26
+ const assembleContentConfigWithConfigSource = Object.assign({}, assembleContentConfig, { configSource })
27
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfigWithConfigSource)
28
+ if (singleConfig) break
29
+ }
30
+ }
21
31
  })
22
32
  }
23
33
 
34
+ function enableKeepSource ({ siteAsciiDocConfig }) {
35
+ if (siteAsciiDocConfig.keepSource instanceof Boolean) return
36
+ siteAsciiDocConfig.keepSource = Object.assign(new Boolean(true), { oldValue: siteAsciiDocConfig.keepSource })
37
+ this.once('navigationBuilt', restoreKeepSource)
38
+ }
39
+
40
+ function restoreKeepSource ({ siteAsciiDocConfig, contentCatalog }) {
41
+ if (!(siteAsciiDocConfig.keepSource instanceof Boolean)) return
42
+ if ((siteAsciiDocConfig.keepSource = siteAsciiDocConfig.keepSource.oldValue)) return
43
+ contentCatalog.getPages((page) => delete page.src.contents)
44
+ }
45
+
24
46
  function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
25
47
  contentCatalog.getComponents().forEach((component) => {
26
48
  component.versions.forEach((componentVersion) => {
27
- const source = componentVersion.nav?.origin
49
+ const source =
50
+ componentVersion.nav?.origin ??
51
+ [...(componentVersion.origins ?? [])].find((it) => it.descriptor?.ext?.assembler)
28
52
  const assemblerConfig = getAssemblerConfigFromDescriptor(source?.descriptor)
29
53
  if (!assemblerConfig) return
30
54
  const componentVersionRef = `${componentVersion.version}@${componentVersion.name}`
@@ -37,7 +61,7 @@ function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
37
61
  Object.entries(entry).forEach(([key, val]) => {
38
62
  if (key === 'profile' || key === 'nav') return
39
63
  const camelKey = key.toLowerCase().replace(/[_-]([a-z0-9])/g, (_, l, idx) => (idx ? l.toUpperCase() : l))
40
- data[camelKey] = val
64
+ if (ASSEMBLY_KEYS.includes(camelKey)) data[camelKey] = val
41
65
  })
42
66
  if (nav) {
43
67
  data.navFiles = (nav.length ? [...new Set(nav)] : nav).reduce((navFiles, path_) => {
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ module.exports = Object.freeze({
4
+ ASSEMBLY_KEYS: [
5
+ 'attributes',
6
+ 'doctype',
7
+ 'dropExplicitXrefText',
8
+ 'linkReferenceStyle',
9
+ 'insertStartPage',
10
+ 'profile',
11
+ 'rootLevel',
12
+ 'sectionMergeStrategy',
13
+ 'xmlIds',
14
+ ],
15
+ LEGACY_ASSEMBLY_KEYS: [
16
+ 'dropExplicitXrefText',
17
+ 'linkReferenceStyle',
18
+ 'insertStartPage',
19
+ 'rootLevel',
20
+ 'sectionMergeStrategy',
21
+ ],
22
+ })
@@ -1,71 +1,34 @@
1
1
  'use strict'
2
2
 
3
- const { compile: bracesToGroup } = require('braces')
4
- const { makeRe: makePicomatchRx } = require('picomatch')
3
+ const { filterCollection, compilePattern } = require('./util/matcher')
5
4
 
6
- const PICOMATCH_OPTS = {
7
- bash: true,
8
- expandRange: (begin, end, step, opts) => bracesToGroup(opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`),
9
- fastpaths: false,
10
- nobracket: true,
11
- noglobstar: true,
12
- nonegate: true,
13
- noquantifiers: true,
14
- regex: false,
15
- strictSlashes: true,
16
- }
17
-
18
- const VERSION_SEPARATOR_RX = /@(?!\()/
19
-
20
- function compilePatterns (patterns) {
21
- if (patterns[0].charAt() === '!') patterns = ['**', ...patterns]
22
- return patterns.map((pattern) => {
23
- const negated = pattern.charAt() === '!'
24
- if (negated) pattern = pattern.slice(1)
25
- let version
26
- const separatorIdx = pattern.search(VERSION_SEPARATOR_RX)
27
- if (~separatorIdx) {
28
- pattern = (version = true) && `${pattern.slice(0, separatorIdx)}%${pattern.slice(separatorIdx + 1) || '*'}`
29
- }
30
- return Object.assign(makePicomatchRx(pattern, PICOMATCH_OPTS), {
31
- globstar: pattern === '**',
32
- negated,
33
- star: pattern === '*',
34
- version,
35
- })
36
- })
37
- }
5
+ const VERSION_SEPARATOR_RX = /@(?!\()(?=(\w)?)/g
6
+ const US = '\x1f'
38
7
 
39
- function filterComponentVersions (components, patterns) {
8
+ function filterComponentVersions (components, { names: patterns, prereleases = true }) {
40
9
  if (!patterns.length) return []
41
- const rxs = compilePatterns(patterns)
42
- return components.reduce((accum, { latest, versions }) => {
43
- accum.push(
44
- ...versions.filter((version) => {
45
- let matched
46
- for (const rx of rxs) {
47
- let voteIfMatched
48
- if (matched) {
49
- if (rx.negated) voteIfMatched = false
50
- } else if (!rx.negated) {
51
- voteIfMatched = true
52
- }
53
- if (voteIfMatched == null) continue
54
- if (rx.globstar) {
55
- matched = voteIfMatched
56
- } else if (rx.star) {
57
- if (version === latest) matched = voteIfMatched
58
- } else if (rx.version) {
59
- if (rx.test(`${version.version}%${version.name}`)) matched = voteIfMatched
60
- } else if (version === latest && rx.test(version.name)) {
61
- matched = voteIfMatched
62
- }
63
- }
64
- return matched
65
- })
66
- )
10
+ const candidateMap = components.reduce((accum, { name: componentName, latest, versions }) => {
11
+ for (const version of versions) {
12
+ if (!prereleases && version.prerelease && version !== latest) continue
13
+ accum[`${version.version}${US}${componentName}`] = { componentName, latest: version === latest, value: version }
14
+ }
67
15
  return accum
68
- }, [])
16
+ }, {})
17
+ if (patterns[0].charAt() === '!') patterns = ['**', ...patterns]
18
+ const result = filterCollection(Object.keys(candidateMap), patterns, {
19
+ compilePattern: new Proxy(compilePattern, {
20
+ apply: (target, self, [pattern]) => {
21
+ if (~pattern.indexOf('@')) pattern = pattern.replace(VERSION_SEPARATOR_RX, (_, c) => US + (c ? '' : '*'))
22
+ return target.call(self, pattern)
23
+ },
24
+ }),
25
+ test: (rx, candidate) => {
26
+ if (rx.globstar) return true
27
+ if (rx.star) return candidateMap[candidate].latest
28
+ return rx.test(candidate) || (candidateMap[candidate].latest && rx.test(candidateMap[candidate].componentName))
29
+ },
30
+ })
31
+ return result.map((it) => candidateMap[it].value)
69
32
  }
70
33
 
71
34
  module.exports = filterComponentVersions