@antora/ui-loader 3.2.0-alpha.3 → 3.2.0-alpha.5

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/lib/constants.js CHANGED
@@ -3,14 +3,13 @@
3
3
  module.exports = Object.freeze({
4
4
  UI_CACHE_FOLDER: 'ui',
5
5
  UI_DESC_FILENAME: 'ui.yml',
6
- UI_SRC_GLOB: '**/*[!~]',
6
+ UI_SRC_GLOB: '**/!(*~)',
7
7
  UI_SRC_OPTS: {
8
+ braceExpansion: false,
8
9
  dot: true,
9
- follow: true,
10
- ignore: ['.git/**'],
11
- nomount: true,
12
- nosort: true,
13
- nounique: true,
14
- strict: false,
10
+ ignore: ['.git'],
11
+ objectMode: true,
12
+ onlyFiles: false,
13
+ unique: false,
15
14
  },
16
15
  })
package/lib/file.js CHANGED
@@ -1,10 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { Readable } = require('stream')
4
- const { Stats } = require('fs')
3
+ const { constants: fsc } = require('node:fs')
4
+ const { posix: path } = require('node:path')
5
+ const { Readable } = require('node:stream')
5
6
  const Vinyl = require('vinyl')
6
7
 
7
8
  const DEFAULT_FILE_MODE = 0o100666 & ~process.umask()
9
+ const invariably = { true: () => true, false: () => false }
8
10
 
9
11
  class File extends Vinyl {
10
12
  get path () {
@@ -27,21 +29,92 @@ class File extends Vinyl {
27
29
  class MemoryFile extends File {
28
30
  constructor (file) {
29
31
  const contents = file.contents || Buffer.alloc(0)
30
- const stat = Object.assign(new Stats(), { mode: DEFAULT_FILE_MODE, mtime: undefined, size: contents.length })
32
+ const stat = {
33
+ mode: DEFAULT_FILE_MODE,
34
+ size: contents.length,
35
+ isDirectory: invariably.false,
36
+ isFile: invariably.true,
37
+ isSymbolicLink: invariably.false,
38
+ }
31
39
  super(Object.assign({}, file, { contents, stat }))
32
40
  }
33
41
  }
34
42
 
35
- class ReadableFile extends Readable {
36
- constructor (file) {
37
- super({ objectMode: true })
38
- this._file = file
43
+ class ZipReadable extends Readable {
44
+ constructor (zipFile, options = {}) {
45
+ super({ objectMode: true, highWaterMark: 1 })
46
+ if ((this._closeable = (this._zipFile = zipFile).reader.fd != null) && !zipFile.autoClose) {
47
+ throw new Error('ZipReadable requires file-based ZipFile to be initialized with autoClose:true option')
48
+ }
49
+ if (!zipFile.lazyEntries) {
50
+ throw new Error('ZipReadable requires ZipFile to be initialized with lazyEntries:true option')
51
+ }
52
+ if ((this._startPath = options.startPath) && (this._startPath = path.join('/', this._startPath + '/')) !== '/') {
53
+ this._startPath = this._startPath.slice(1)
54
+ } else {
55
+ this._startPath = undefined
56
+ }
57
+ this._init()
58
+ }
59
+
60
+ _init () {
61
+ const zipFile = this._zipFile
62
+ zipFile
63
+ .on('entry', (entry) => {
64
+ const mode = this.getFileMode(entry)
65
+ if ((mode & fsc.S_IFMT) === fsc.S_IFDIR) return zipFile.readEntry()
66
+ let path_ = entry.fileName
67
+ if (this._startPath) {
68
+ if (path_.length < this._startPath.length || !path_.startsWith(this._startPath)) return zipFile.readEntry()
69
+ path_ = path_.slice(this._startPath.length)
70
+ }
71
+ const isLink = (mode & fsc.S_IFMT) === fsc.S_IFLNK
72
+ const stat = {
73
+ mode,
74
+ mtime: entry.getLastModDate(),
75
+ size: entry.uncompressedSize,
76
+ isDirectory: invariably.false,
77
+ isFile: invariably[!isLink],
78
+ isSymbolicLink: invariably[isLink],
79
+ }
80
+ const file = { path: path_, stat }
81
+ if (stat.size === 0) {
82
+ file.contents = Buffer.alloc(0)
83
+ this.push(new File(file))
84
+ } else {
85
+ zipFile.openReadStream(entry, (readErr, readStream) => {
86
+ if (readErr) {
87
+ zipFile.close()
88
+ this.emit('error', readErr)
89
+ return
90
+ }
91
+ if (isLink) {
92
+ const buffer = []
93
+ readStream
94
+ .on('data', (chunk) => buffer.push(chunk))
95
+ .on('error', (readStreamErr) => this.emit('error', readStreamErr))
96
+ .on('end', () => {
97
+ file.symlink = (buffer.length === 1 ? buffer[0] : Buffer.concat(buffer)).toString()
98
+ this.push(new File(file))
99
+ })
100
+ } else {
101
+ file.contents = readStream
102
+ this.push(new File(file))
103
+ }
104
+ })
105
+ }
106
+ })
107
+ .on(this._closeable ? 'close' : 'end', () => zipFile.emittedError || this.push(null))
108
+ }
109
+
110
+ _read (_n) {
111
+ this._zipFile.readEntry()
39
112
  }
40
113
 
41
- _read () {
42
- this.push(this._file)
43
- this.push((this._file = null))
114
+ getFileMode ({ externalFileAttributes }) {
115
+ const attr = externalFileAttributes >> 16 || 33188
116
+ return [448, 56, 7].map((mask) => attr & mask).reduce((a, b) => a + b, attr & fsc.S_IFMT)
44
117
  }
45
118
  }
46
119
 
47
- module.exports = { File, MemoryFile, ReadableFile }
120
+ module.exports = { File, MemoryFile, ZipReadable }
package/lib/load-ui.js CHANGED
@@ -1,23 +1,25 @@
1
1
  'use strict'
2
2
 
3
3
  const { compile: bracesToGroup } = require('braces')
4
- const { createHash } = require('crypto')
4
+ const { createHash } = require('node:crypto')
5
5
  const expandPath = require('@antora/expand-path-helper')
6
- const { File, MemoryFile, ReadableFile } = require('./file')
7
- const { promises: fsp } = require('fs')
6
+ const { File, MemoryFile, ZipReadable } = require('./file')
7
+ const { promises: fsp } = require('node:fs')
8
8
  const { concat: get } = require('simple-get')
9
9
  const getCacheDir = require('cache-directory')
10
- const globStream = require('glob-stream')
11
- const ospath = require('path')
10
+ const { globStream } = require('fast-glob')
11
+ const { inspect } = require('node:util')
12
+ const invariably = { false: () => false, void: () => undefined }
13
+ const ospath = require('node:path')
12
14
  const { posix: path } = ospath
13
15
  const picomatch = require('picomatch')
14
16
  const posixify = ospath.sep === '\\' ? (p) => p.replace(/\\/g, '/') : undefined
15
- const { pipeline, Transform, Writable } = require('stream')
17
+ const { pipeline, PassThrough, Writable } = require('node:stream')
16
18
  const forEach = (write, final) => new Writable({ objectMode: true, write, final })
17
- const map = (transform) => new Transform({ objectMode: true, transform })
19
+ const through = () => new PassThrough({ objectMode: true })
18
20
  const UiCatalog = require('./ui-catalog')
19
21
  const yaml = require('js-yaml')
20
- const vzip = require('@vscode/gulp-vinyl-zip')
22
+ const yauzl = require('yauzl')
21
23
 
22
24
  const STATIC_FILE_MATCHER_OPTS = {
23
25
  expandRange: (begin, end, step, opts) => bracesToGroup(opts ? `{${begin}..${end}..${step}}` : `{${begin}..${end}}`),
@@ -103,20 +105,15 @@ async function loadUi (playbook) {
103
105
  new Promise((resolve, reject) =>
104
106
  bundleFile.isDirectory()
105
107
  ? srcFs(ospath.join(bundleFile.path, bundle.startPath || '', '.')).then(resolve, reject)
106
- : vzip
107
- .src(bundleFile.path)
108
+ : srcZip(bundleFile.path, { startPath: bundle.startPath })
108
109
  .on('error', (err) => reject(Object.assign(err, { message: `not a valid zip file; ${err.message}` })))
109
- .pipe(selectFilesStartingFrom(bundle.startPath))
110
- .pipe(bufferizeContents())
110
+ .pipe(bufferizeContentsAndCollectFiles(resolve))
111
111
  .on('error', reject)
112
- .pipe(collectFiles(resolve))
113
112
  ).catch((err) => {
114
- const errWrapper = new Error(
113
+ const msg =
115
114
  `Failed to read UI ${bundleFile.isDirectory() ? 'directory' : 'bundle'}: ` +
116
- (bundleUrl === bundleFile.path ? bundleUrl : `${bundleFile.path} (resolved from url: ${bundleUrl})`)
117
- )
118
- errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}`
119
- throw errWrapper
115
+ (bundleUrl === bundleFile.path ? bundleUrl : `${bundleFile.path} (resolved from url: ${bundleUrl})`)
116
+ throw transformError(err, msg)
120
117
  })
121
118
  ),
122
119
  srcSupplementalFiles(supplementalFilesSpec, startDir),
@@ -180,8 +177,7 @@ function downloadBundle (url, to, agent) {
180
177
  const message = `Response code ${response.statusCode} (${response.statusMessage})`
181
178
  return reject(Object.assign(new Error(message), { name: 'HTTPError' }))
182
179
  }
183
- new ReadableFile(new MemoryFile({ path: ospath.basename(to), contents }))
184
- .pipe(vzip.src())
180
+ srcZip(contents, { testOnly: true })
185
181
  .on('error', (err) =>
186
182
  reject(Object.assign(err, { message: `not a valid zip file; ${err.message}`, summary: 'Invalid UI bundle' }))
187
183
  )
@@ -189,74 +185,18 @@ function downloadBundle (url, to, agent) {
189
185
  fsp
190
186
  .mkdir(ospath.dirname(to), { recursive: true })
191
187
  .then(() => fsp.writeFile(to, contents))
192
- .then(() => resolve(new File({ path: to, stat: { isDirectory: () => false } })))
188
+ .then(() => resolve(new File({ path: to, stat: { isDirectory: invariably.false } })))
193
189
  )
194
190
  })
195
191
  }).catch((err) => {
196
- const errWrapper = new Error(`${err.summary || 'Failed to download UI bundle'}: ${url}`)
192
+ const errWrapper = transformError(err, `${err.summary || 'Failed to download UI bundle'}: ${url}`)
197
193
  if (err.code === 'ECONNRESET' || (err.message || '').toLowerCase() === 'request timed out') {
198
- errWrapper.recoverable = true
194
+ Object.defineProperty(errWrapper, 'recoverable', { value: true })
199
195
  }
200
- throw Object.assign(errWrapper, { stack: `${errWrapper.stack}\nCaused by: ${err.stack || 'unknown'}` })
196
+ throw errWrapper
201
197
  })
202
198
  }
203
199
 
204
- function selectFilesStartingFrom (startPath) {
205
- if (!startPath || (startPath = path.join('/', startPath + '/')) === '/') {
206
- return map((file, _, next) => {
207
- if (file.isNull()) {
208
- next()
209
- } else {
210
- next(
211
- null,
212
- new File({ path: posixify ? posixify(file.path) : file.path, contents: file.contents, stat: file.stat })
213
- )
214
- }
215
- })
216
- } else {
217
- startPath = startPath.substr(1)
218
- const startPathOffset = startPath.length
219
- return map((file, _, next) => {
220
- if (file.isNull()) {
221
- next()
222
- } else {
223
- const path_ = posixify ? posixify(file.path) : file.path
224
- if (path_.length > startPathOffset && path_.startsWith(startPath)) {
225
- next(null, new File({ path: path_.substr(startPathOffset), contents: file.contents, stat: file.stat }))
226
- } else {
227
- next()
228
- }
229
- }
230
- })
231
- }
232
- }
233
-
234
- function bufferizeContents () {
235
- return map((file, _, next) => {
236
- // NOTE gulp-vinyl-zip automatically converts the contents of an empty file to a Buffer
237
- if (file.isStream()) {
238
- const buffer = []
239
- pipeline(
240
- file.contents,
241
- forEach((chunk, _, done) => buffer.push(chunk) && done()),
242
- (err) => (err ? next(err) : next(null, Object.assign(file, { contents: Buffer.concat(buffer) })))
243
- )
244
- } else {
245
- next(null, file)
246
- }
247
- })
248
- }
249
-
250
- function collectFiles (resolve, files = new Map()) {
251
- return forEach(
252
- (file, _, done) => {
253
- files.set(file.path, file)
254
- done()
255
- },
256
- (done) => done() || resolve(files)
257
- )
258
- }
259
-
260
200
  function srcSupplementalFiles (filesSpec, startDir) {
261
201
  if (!filesSpec) return new Map()
262
202
  let cwd
@@ -291,9 +231,7 @@ function srcSupplementalFiles (filesSpec, startDir) {
291
231
  if (err.code === 'ENOENT' && err.path === cwd) {
292
232
  throw new Error(`Specified ui.supplemental_files directory does not exist: ${dir}`)
293
233
  } else {
294
- const errWrapper = new Error(`Failed to read ui.supplemental_files ${cwd ? `directory: ${dir}` : 'entry'}`)
295
- errWrapper.stack += `\nCaused by: ${err.stack || 'unknown'}`
296
- throw errWrapper
234
+ throw transformError(err, `Failed to read ui.supplemental_files ${cwd ? `directory: ${dir}` : 'entry'}`)
297
235
  }
298
236
  })
299
237
  }
@@ -355,17 +293,15 @@ function resolveOut (file, outputDir = '_') {
355
293
  }
356
294
 
357
295
  function srcFs (cwd) {
358
- return new Promise((resolve, reject, cache = Object.create(null), files = new Map(), relpathStart = cwd.length + 1) =>
296
+ return new Promise((resolve, reject, files = new Map()) =>
359
297
  pipeline(
360
- globStream(UI_SRC_GLOB, Object.assign({ cache, cwd }, UI_SRC_OPTS)),
361
- forEach(({ path: abspathPosix }, _, done) => {
362
- if ((cache[abspathPosix] || {}).constructor === Array) return done() // detects some directories
363
- const abspath = posixify ? ospath.normalize(abspathPosix) : abspathPosix
364
- const relpath = abspath.substr(relpathStart)
365
- symlinkAwareStat(abspath).then(
298
+ globStream(UI_SRC_GLOB, Object.assign({ cwd }, UI_SRC_OPTS)),
299
+ forEach(({ path: relpath, dirent }, _, done) => {
300
+ if (dirent.isDirectory()) return done()
301
+ const relpathPosix = relpath
302
+ const abspath = posixify ? ospath.join(cwd, (relpath = ospath.normalize(relpath))) : cwd + '/' + relpath
303
+ fsp.stat(abspath).then(
366
304
  (stat) => {
367
- if (stat.isDirectory()) return done() // detects directories that slipped through cache check
368
- const relpathPosix = posixify ? posixify(relpath) : relpath
369
305
  fsp.readFile(abspath).then(
370
306
  (contents) => {
371
307
  files.set(relpathPosix, new File({ cwd, path: relpathPosix, contents, stat, local: true }))
@@ -376,16 +312,18 @@ function srcFs (cwd) {
376
312
  }
377
313
  )
378
314
  },
379
- (statErr) => {
380
- done(
381
- Object.assign(statErr, {
382
- message: statErr.symlink
383
- ? (statErr.code === 'ELOOP' ? 'ELOOP: symbolic link cycle, ' : 'ENOENT: broken symbolic link, ') +
384
- `${relpath} -> ${statErr.symlink}`
385
- : statErr.message.replace(`'${abspath}'`, relpath),
386
- })
387
- )
388
- }
315
+ (statErr) =>
316
+ dirent.isSymbolicLink()
317
+ ? fsp
318
+ .readlink(abspath)
319
+ .then(
320
+ (symlink) =>
321
+ (statErr.code === 'ELOOP' ? 'ELOOP: symbolic link cycle, ' : 'ENOENT: broken symbolic link, ') +
322
+ `${relpath} -> ${symlink}`,
323
+ () => statErr.message.replace(`'${abspath}'`, relpath)
324
+ )
325
+ .then((message) => done(Object.assign(statErr, { message })))
326
+ : done(Object.assign(statErr, { message: statErr.message.replace(`'${abspath}'`, relpath) }))
389
327
  )
390
328
  }),
391
329
  (err) => (err ? reject(err) : resolve(files))
@@ -393,18 +331,42 @@ function srcFs (cwd) {
393
331
  )
394
332
  }
395
333
 
396
- function symlinkAwareStat (path_) {
397
- return fsp.lstat(path_).then((lstat) => {
398
- if (!lstat.isSymbolicLink()) return lstat
399
- return fsp.stat(path_).catch((statErr) =>
400
- fsp
401
- .readlink(path_)
402
- .catch(() => undefined)
403
- .then((symlink) => {
404
- throw Object.assign(statErr, { symlink })
405
- })
406
- )
334
+ function srcZip (file, options = {}) {
335
+ const result = options.testOnly // is it necessary to close streams in this case, or just sink()?
336
+ ? forEach((file_, _, done) => (file_.isStream() ? file_.contents.on('close', done).destroy() : done()))
337
+ : through()
338
+ yauzl[file instanceof Buffer ? 'fromBuffer' : 'open'](file, { lazyEntries: true }, (err, zipFile) => {
339
+ if (err) return result.emit('error', err)
340
+ new ZipReadable(zipFile, options).pipe(result)
407
341
  })
342
+ return result
343
+ }
344
+
345
+ function bufferizeContentsAndCollectFiles (resolve, files = new Map()) {
346
+ return forEach(
347
+ (file, _, done) => {
348
+ if (file.isStream()) {
349
+ const buffer = []
350
+ file.contents
351
+ .on('data', (chunk) => buffer.push(chunk))
352
+ .on('end', () => {
353
+ file.contents = buffer.length === 1 ? buffer[0] : Buffer.concat(buffer)
354
+ files.set(file.path, file)
355
+ done()
356
+ })
357
+ } else {
358
+ files.set(file.path, file)
359
+ done()
360
+ }
361
+ },
362
+ (done) => done() || resolve(files)
363
+ )
364
+ }
365
+
366
+ function transformError (err, msg) {
367
+ const errWrapper = new Error(msg)
368
+ errWrapper.stack += `\nCaused by: ${err.stack ? inspect(err).replace(/^Error \[(.+)\](?=: )/, '$1') : err}`
369
+ return errWrapper
408
370
  }
409
371
 
410
372
  module.exports = loadUi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/ui-loader",
3
- "version": "3.2.0-alpha.3",
3
+ "version": "3.2.0-alpha.5",
4
4
  "description": "Downloads a UI bundle, if necessary, and loads the files into a UI catalog for use in an Antora documentation pipeline.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -26,19 +26,19 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@antora/expand-path-helper": "~2.0",
29
- "@vscode/gulp-vinyl-zip": "~2.5",
30
29
  "braces": "~3.0",
31
30
  "cache-directory": "~2.0",
32
- "glob-stream": "~7.0",
31
+ "fast-glob": "~3.3",
33
32
  "hpagent": "~1.2",
34
33
  "js-yaml": "~4.1",
35
- "picomatch": "~2.3",
34
+ "picomatch": "~4.0",
36
35
  "should-proxy": "~1.0",
37
36
  "simple-get": "~4.0",
38
- "vinyl": "~2.2"
37
+ "vinyl": "~3.0",
38
+ "yauzl": "~3.1"
39
39
  },
40
40
  "engines": {
41
- "node": ">=16.0.0"
41
+ "node": ">=18.0.0"
42
42
  },
43
43
  "files": [
44
44
  "lib/"