js-build-tools 6.1.0 → 6.2.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.
Files changed (58) hide show
  1. package/README.md +89 -25
  2. package/dist/build.js +5 -2
  3. package/dist/build.min.js +1 -1
  4. package/dist/bundle.js +40 -40
  5. package/dist/bundleMinify.js +18 -18
  6. package/dist/copyFiles.js +47 -0
  7. package/dist/copyFiles.min.js +1 -0
  8. package/dist/copyFonts.js +13 -13
  9. package/dist/default.js +15 -15
  10. package/dist/distMinify.js +13 -13
  11. package/dist/docs.js +19 -0
  12. package/dist/docs.min.js +1 -0
  13. package/dist/jsdocDocs.js +15 -0
  14. package/dist/jsdocDocs.min.js +1 -0
  15. package/dist/partials/addToReadme.js +25 -25
  16. package/dist/partials/beginWatcher.js +13 -13
  17. package/dist/partials/clean.js +15 -15
  18. package/dist/partials/copyFor.js +10 -2
  19. package/dist/partials/distFor.js +22 -22
  20. package/dist/partials/distForSrc.js +12 -12
  21. package/dist/partials/distSeries.js +20 -20
  22. package/dist/partials/imagesFor.js +43 -43
  23. package/dist/partials/jsdocDocsFor.js +155 -0
  24. package/dist/partials/neostandardLint.js +121 -121
  25. package/dist/partials/readmeTemplate.js +15 -15
  26. package/dist/partials/resolveDocsMethod.js +19 -0
  27. package/dist/partials/runOnChange.js +29 -29
  28. package/dist/partials/sassFor.js +29 -29
  29. package/dist/partials/tsFor.js +71 -71
  30. package/dist/partials/typeDocsFor.js +203 -203
  31. package/dist/partials.js +149 -135
  32. package/dist/partials.min.js +1 -1
  33. package/dist/sourceLint.js +26 -26
  34. package/dist/test-helpers/neostandardLintWithRealNeostandard.js +49 -49
  35. package/dist/test-helpers/setUp.js +95 -95
  36. package/dist/testFull.js +20 -20
  37. package/dist/testHelpers.js +10 -10
  38. package/dist/testQuick.js +22 -22
  39. package/dist/utilities/dotNotate.js +48 -48
  40. package/dist/utilities/getBundleConfigs.js +28 -28
  41. package/dist/watchTest.js +16 -16
  42. package/functions/build.mjs +5 -2
  43. package/functions/build.test.mjs +50 -1
  44. package/functions/copyFiles.mjs +32 -0
  45. package/functions/copyFiles.test.mjs +78 -0
  46. package/functions/docs.mjs +13 -0
  47. package/functions/docs.test.mjs +45 -0
  48. package/functions/jsdocDocs.mjs +10 -0
  49. package/functions/partials/copyFor.mjs +4 -1
  50. package/functions/partials/jsdocDocsFor.mjs +132 -0
  51. package/functions/partials/jsdocDocsFor.test.mjs +101 -0
  52. package/functions/partials/resolveDocsMethod.mjs +10 -0
  53. package/functions/partials/resolveDocsMethod.test.mjs +33 -0
  54. package/functions/partials.mjs +2 -0
  55. package/functions/partials.test.mjs +1 -1
  56. package/gulp.config.mjs +28 -5
  57. package/gulpfile.base.mjs +4 -1
  58. package/package.json +14 -1
@@ -4,6 +4,7 @@ import { build } from './build.mjs'
4
4
  import { clean } from './partials/clean.mjs'
5
5
  import { testFull } from './testFull.mjs'
6
6
  import { typeDocs } from './typeDocs.mjs'
7
+ import { jsdocDocs } from './jsdocDocs.mjs'
7
8
  import { countMatches } from 'test-filesystem'
8
9
 
9
10
  // Run the real neostandard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
@@ -16,8 +17,11 @@ const genericFunction = () => Promise.resolve(true)
16
17
 
17
18
  jest.mock('./partials/clean.mjs', () => ({ clean: jest.fn(genericFunction) }))
18
19
  jest.mock('./testFull.mjs', () => ({ testFull: jest.fn(genericFunction) }))
19
- // The documentation is generated by TypeDoc, whose own tests cover that (see typeDocsFor.test.mjs)
20
+ // The documentation is generated by TypeDoc or jsdoc, whose own tests cover that (see typeDocsFor.test.mjs /
21
+ // jsdocDocsFor.test.mjs) - jsdocDocs in particular shells out to a real CLI, far too slow for a build.mjs unit
22
+ // test to actually run.
20
23
  jest.mock('./typeDocs.mjs', () => ({ typeDocs: jest.fn(genericFunction) }))
24
+ jest.mock('./jsdocDocs.mjs', () => ({ jsdocDocs: jest.fn(genericFunction) }))
21
25
  // tsFor's real implementation loads gulp-ts-compile (a genuine ESM-only package) via a dynamic import() hidden
22
26
  // from babel's static analysis - necessary for production correctness (see tsFor.mjs's own comments), but this
23
27
  // project's current CommonJS-transform-based Jest setup can't execute a real dynamic import at all: it doesn't
@@ -638,6 +642,7 @@ describe('build', () => {
638
642
  expect.assertions(3)
639
643
  build(() => {
640
644
  gulpConfig.set('docs.enabled', false)
645
+ gulpConfig.set('typescript.enabled', false)
641
646
  expect(typeDocs).toHaveBeenCalledTimes(1)
642
647
  // The readme is not generated, it is written by hand
643
648
  expect(fs.existsSync(`${readmePath}/${readmeFile}`)).toBe(false)
@@ -646,6 +651,50 @@ describe('build', () => {
646
651
  })
647
652
  }, 30000)
648
653
 
654
+ test('when docs.enabled is set without typescript.enabled: generates the documentation with jsdoc, inferred from docs.method being unset', done => {
655
+ const readmePath = gulpConfig.get('readme.to')
656
+ const readmeFile = gulpConfig.get('readme.file')
657
+ fs.rmSync(`${readmePath}/${readmeFile}`, { force: true })
658
+ jsdocDocs.mockClear()
659
+ gulpConfig.set('browser.enabled', false)
660
+ gulpConfig.set('typescript.enabled', false)
661
+ gulpConfig.set('docs.enabled', true)
662
+ expect.assertions(3)
663
+ build(() => {
664
+ gulpConfig.set('docs.enabled', false)
665
+ expect(jsdocDocs).toHaveBeenCalledTimes(1)
666
+ // The readme is not generated, it is written by hand - same as the TypeDoc path above
667
+ expect(fs.existsSync(`${readmePath}/${readmeFile}`)).toBe(false)
668
+ expect(testFull).toHaveBeenCalled()
669
+ done()
670
+ })
671
+ }, 30000)
672
+
673
+ test('docs.method overrides the typescript.enabled inference either way', done => {
674
+ const srcPath = gulpConfig.get('srcPath')
675
+ const rootPath = gulpConfig.get('rootPath')
676
+ const tsConfig = `${rootPath}/tsconfig.json`
677
+ fs.writeFileSync(`${srcPath}/main.ts`, mainTypescript)
678
+ fs.writeFileSync(tsConfig, tsConfigContents)
679
+ typeDocs.mockClear()
680
+ jsdocDocs.mockClear()
681
+ gulpConfig.set('browser.enabled', false)
682
+ gulpConfig.set('typescript.config', tsConfig)
683
+ gulpConfig.set('typescript.enabled', true)
684
+ gulpConfig.set('docs.enabled', true)
685
+ // typescript.enabled is true, which would normally infer 'typedoc' - method forces 'jsdoc' instead.
686
+ gulpConfig.set('docs.method', 'jsdoc')
687
+ expect.assertions(2)
688
+ build(() => {
689
+ gulpConfig.set('docs.enabled', false)
690
+ gulpConfig.set('docs.method', false)
691
+ gulpConfig.set('typescript.enabled', false)
692
+ expect(jsdocDocs).toHaveBeenCalledTimes(1)
693
+ expect(typeDocs).not.toHaveBeenCalled()
694
+ done()
695
+ })
696
+ }, 30000)
697
+
649
698
  test('finishes clean before starting the tasks that write into the output folders', done => {
650
699
  const srcPath = gulpConfig.get('srcPath')
651
700
  const sourcesPath = `${srcPath}/sources`
@@ -0,0 +1,32 @@
1
+ import { copyFor } from './partials/copyFor.mjs'
2
+ import * as gulpConfig from '../gulp.config.mjs'
3
+
4
+ /**
5
+ * Copy files from one place to another - any number of { from, to, base? } entries, configured via copyFiles.
6
+ * A project with no copyFiles entries gets a task that does nothing. Meant to be run explicitly (its own script,
7
+ * e.g. `gulp copyFiles`), not folded into the default build - a bundle (or any other file this copies) is often
8
+ * the live, publicly-reachable version of something, so publishing it elsewhere should be a deliberate step, not
9
+ * a side effect of every routine local build.
10
+ *
11
+ * Reuses copyFor (already shared with copyFonts) rather than a fresh src()/dest() pair - that keeps its existing
12
+ * encoding: false fix (Gulp 5's default utf8 handling corrupts binary files such as fonts or a minified bundle)
13
+ * instead of silently losing it in a parallel implementation.
14
+ *
15
+ * Each entry runs as its own independent copyFor(), awaited via Promise.all rather than merged into one combined
16
+ * stream (mergeStream, the pattern bundle.mjs uses) - that pattern assumes every merged stream ends up at the
17
+ * same shared destination; here, each entry can have a genuinely different one, and merging streams that each
18
+ * already terminate at their own separate dest() hit a real "write after end" error in practice.
19
+ * @memberOf module:js-build-tools
20
+ * @returns {Promise<void>}
21
+ */
22
+ export const copyFiles = () => {
23
+ const copies = gulpConfig.get('copyFiles', [])
24
+ if (!copies.length) {
25
+ return Promise.resolve()
26
+ }
27
+ return Promise.all(copies.map(({ from, to, base }) => new Promise((resolve, reject) => {
28
+ copyFor(from, to, { base })
29
+ .on('finish', resolve)
30
+ .on('error', reject)
31
+ }))).then(() => {})
32
+ }
@@ -0,0 +1,78 @@
1
+ import fs from 'fs'
2
+ import * as setUp from './test-helpers/setUp.mjs'
3
+ import { copyFiles } from './copyFiles.mjs'
4
+
5
+ setUp.setDefaults('test-copy-files')
6
+ const gulpConfig = setUp.gulpConfig
7
+
8
+ beforeEach(() => setUp.beforeEach())
9
+ afterEach(() => {
10
+ gulpConfig.set('copyFiles', [])
11
+ return setUp.afterEach()
12
+ })
13
+
14
+ describe('copyFiles', () => {
15
+ test('with no entries configured, does nothing and resolves', async () => {
16
+ await expect(copyFiles()).resolves.toBeUndefined()
17
+ })
18
+
19
+ test('copies a file from one place to another', async () => {
20
+ const sourceDir = `${gulpConfig.get('rootPath')}copy-source`
21
+ const destDir = `${gulpConfig.get('rootPath')}copy-dest`
22
+ fs.mkdirSync(sourceDir, { recursive: true })
23
+ fs.writeFileSync(`${sourceDir}/one.txt`, 'one file\n')
24
+ gulpConfig.set('copyFiles', [{ from: `${sourceDir}/*.txt`, to: destDir }])
25
+
26
+ await copyFiles()
27
+
28
+ expect(fs.readFileSync(`${destDir}/one.txt`).toString()).toBe('one file\n')
29
+ })
30
+
31
+ test('copies several configured entries, each to its own destination', async () => {
32
+ const sourceDirA = `${gulpConfig.get('rootPath')}copy-source-a`
33
+ const sourceDirB = `${gulpConfig.get('rootPath')}copy-source-b`
34
+ const destDirA = `${gulpConfig.get('rootPath')}copy-dest-a`
35
+ const destDirB = `${gulpConfig.get('rootPath')}copy-dest-b`
36
+ fs.mkdirSync(sourceDirA, { recursive: true })
37
+ fs.mkdirSync(sourceDirB, { recursive: true })
38
+ fs.writeFileSync(`${sourceDirA}/a.txt`, 'from a\n')
39
+ fs.writeFileSync(`${sourceDirB}/b.txt`, 'from b\n')
40
+ gulpConfig.set('copyFiles', [
41
+ { from: `${sourceDirA}/*.txt`, to: destDirA },
42
+ { from: `${sourceDirB}/*.txt`, to: destDirB }
43
+ ])
44
+
45
+ await copyFiles()
46
+
47
+ expect(fs.readFileSync(`${destDirA}/a.txt`).toString()).toBe('from a\n')
48
+ expect(fs.readFileSync(`${destDirB}/b.txt`).toString()).toBe('from b\n')
49
+ })
50
+
51
+ test('base preserves the glob\'s own subdirectory structure in the destination', async () => {
52
+ const sourceDir = `${gulpConfig.get('rootPath')}copy-source-nested`
53
+ const destDir = `${gulpConfig.get('rootPath')}copy-dest-nested`
54
+ fs.mkdirSync(`${sourceDir}/inner`, { recursive: true })
55
+ fs.writeFileSync(`${sourceDir}/inner/deep.txt`, 'nested file\n')
56
+ gulpConfig.set('copyFiles', [{ from: `${sourceDir}/**/*.txt`, to: destDir, base: sourceDir }])
57
+
58
+ await copyFiles()
59
+
60
+ expect(fs.readFileSync(`${destDir}/inner/deep.txt`).toString()).toBe('nested file\n')
61
+ })
62
+
63
+ // The real bug this guards against: a fresh src()/dest() pair (what this used to do) loses copyFor.mjs's own
64
+ // encoding: false fix - Gulp 5's default utf8 handling corrupts binary files such as fonts or a minified
65
+ // bundle. Reusing copyFor instead of duplicating its logic keeps that fix automatically.
66
+ test('copies binary files byte for byte, including bytes above 0x7f', async () => {
67
+ const sourceDir = `${gulpConfig.get('rootPath')}copy-source-binary`
68
+ const destDir = `${gulpConfig.get('rootPath')}copy-dest-binary`
69
+ fs.mkdirSync(sourceDir, { recursive: true })
70
+ const original = Buffer.from(Array.from({ length: 512 }, (_, i) => i % 256))
71
+ fs.writeFileSync(`${sourceDir}/font.woff`, original)
72
+ gulpConfig.set('copyFiles', [{ from: `${sourceDir}/*.woff`, to: destDir }])
73
+
74
+ await copyFiles()
75
+
76
+ expect(fs.readFileSync(`${destDir}/font.woff`).equals(original)).toBe(true)
77
+ })
78
+ })
@@ -0,0 +1,13 @@
1
+ import { jsdocDocs } from './jsdocDocs.mjs'
2
+ import { resolveDocsMethod } from './partials/resolveDocsMethod.mjs'
3
+ import { typeDocs } from './typeDocs.mjs'
4
+
5
+ /**
6
+ * Generate the HTML documentation with whichever engine docs.method (or its typescript.enabled-based default)
7
+ * resolves to - see {@link module:partials.resolveDocsMethod}. The standalone counterpart to build's own
8
+ * internal branching, for running `gulp docs` directly instead of as part of a full build.
9
+ * @memberOf module:js-build-tools
10
+ * @param {Function} [done]
11
+ * @returns {*}
12
+ */
13
+ export const docs = (done = null) => (resolveDocsMethod() === 'jsdoc' ? jsdocDocs : typeDocs)(done)
@@ -0,0 +1,45 @@
1
+ import * as setUp from './test-helpers/setUp.mjs'
2
+ import { docs } from './docs.mjs'
3
+ import { typeDocs } from './typeDocs.mjs'
4
+ import { jsdocDocs } from './jsdocDocs.mjs'
5
+
6
+ const genericFunction = () => Promise.resolve(true)
7
+
8
+ jest.mock('./typeDocs.mjs', () => ({ typeDocs: jest.fn(genericFunction) }))
9
+ jest.mock('./jsdocDocs.mjs', () => ({ jsdocDocs: jest.fn(genericFunction) }))
10
+
11
+ setUp.setDefaults('test-docs')
12
+ const gulpConfig = setUp.gulpConfig
13
+
14
+ beforeEach(() => setUp.beforeEach().then(() => {
15
+ typeDocs.mockClear()
16
+ jsdocDocs.mockClear()
17
+ }))
18
+
19
+ afterEach(setUp.afterEach)
20
+
21
+ describe('docs', () => {
22
+ test('runs jsdocDocs when resolveDocsMethod infers jsdoc', async () => {
23
+ gulpConfig.set('typescript.enabled', false)
24
+ gulpConfig.set('docs.method', false)
25
+ await docs()
26
+ expect(jsdocDocs).toHaveBeenCalledTimes(1)
27
+ expect(typeDocs).not.toHaveBeenCalled()
28
+ })
29
+
30
+ test('runs typeDocs when resolveDocsMethod infers typedoc', async () => {
31
+ gulpConfig.set('typescript.enabled', true)
32
+ gulpConfig.set('docs.method', false)
33
+ await docs()
34
+ expect(typeDocs).toHaveBeenCalledTimes(1)
35
+ expect(jsdocDocs).not.toHaveBeenCalled()
36
+ })
37
+
38
+ test('docs.method overrides the typescript.enabled inference either way', async () => {
39
+ gulpConfig.set('typescript.enabled', true)
40
+ gulpConfig.set('docs.method', 'jsdoc')
41
+ await docs()
42
+ expect(jsdocDocs).toHaveBeenCalledTimes(1)
43
+ expect(typeDocs).not.toHaveBeenCalled()
44
+ })
45
+ })
@@ -0,0 +1,10 @@
1
+ import { jsdocDocsFor } from './partials/jsdocDocsFor.mjs'
2
+
3
+ /**
4
+ * Generate the HTML documentation of a plain-JavaScript project from its source, with a modern jsdoc theme
5
+ * instead of jsdoc's own classic default. Turn it on with docs.enabled and docs.method: 'jsdoc' (the default
6
+ * when typescript.enabled is off), see {@link module:partials.jsdocDocsFor}.
7
+ * @memberOf module:js-build-tools
8
+ * @returns {Function}
9
+ */
10
+ export const jsdocDocs = jsdocDocsFor()
@@ -5,8 +5,11 @@ import { dest, src } from 'gulp'
5
5
  * @memberOf module:partials
6
6
  * @param {string|array} srcPath
7
7
  * @param {string} destPath
8
+ * @param {Object} [options]
9
+ * @param {string} [options.base] - Passed through to gulp's own src(pattern, {base}), to preserve srcPath's own
10
+ * subdirectory structure in destPath instead of flattening it.
8
11
  * @returns {stream.Stream}
9
12
  */
10
13
  // encoding: false copies the bytes untouched - Gulp 5's default utf8 handling corrupts binary files such as fonts.
11
- export const copyFor = (srcPath, destPath) => src(srcPath, { encoding: false })
14
+ export const copyFor = (srcPath, destPath, { base } = {}) => src(srcPath, { encoding: false, ...(base ? { base } : {}) })
12
15
  .pipe(dest(destPath))
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { createRequire } from 'node:module'
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+ import * as gulpConfig from '../../gulp.config.mjs'
8
+ import { removeDirectory } from './removeDirectory.mjs'
9
+ // jsdoc.base.js is CommonJS (it's also required directly by consuming projects' own .jsdoc.conf.js, and by this
10
+ // project's own legacy htmldocs script) - Node's ESM loader imports a CJS module's module.exports as the default
11
+ // export directly, no createRequire/import.meta needed (import.meta.url tripped up this project's own Babel CJS
12
+ // transform, which Jest relies on to run .mjs test files - confirmed by removing it and nothing else changing).
13
+ import jsdocBase from '../../jsdoc.base.js'
14
+
15
+ const execFileAsync = promisify(execFile)
16
+
17
+ const jsdocInstallHint = 'Install it in your project with: npm install --save-dev jsdoc'
18
+ const themeInstallHint = name => `Install it in your project with: npm install --save-dev ${name}`
19
+
20
+ /**
21
+ * Resolve the installed directory of an optional peer dependency, from the project being built rather than from
22
+ * this package. Deliberately not requirePeer/importPeer's own `require(name + '/package.json')` approach - that
23
+ * breaks for any package whose own package.json restricts its exports map and does not expose that subpath
24
+ * (clean-jsdoc-theme does; confirmed live, not assumed - resolving its own package.json throws
25
+ * ERR_PACKAGE_PATH_NOT_EXPORTED in plain Node, and a differently-shaped MODULE_NOT_FOUND under Jest's own
26
+ * resolver, which still is not "not installed" either way). Walking Node's own real module-lookup directories
27
+ * (require.resolve.paths) and checking each on disk sidesteps the exports map entirely - this only ever reads
28
+ * whether `<dir>/<name>/package.json` exists, never asks the module system to resolve a restricted subpath.
29
+ * @memberOf module:partials
30
+ * @param {string} name - The package name.
31
+ * @param {Object} options
32
+ * @param {string} options.task
33
+ * @param {string} options.installHint
34
+ * @param {string} [options.projectPath=process.cwd()]
35
+ * @returns {string}
36
+ * @throws {Error} With install instructions when the package is not installed.
37
+ */
38
+ const resolvePeerDir = (name, { task, installHint, projectPath = process.cwd() }) => {
39
+ const req = createRequire(path.join(projectPath, 'package.json'))
40
+ const found = (req.resolve.paths(name) || [])
41
+ .map(dir => path.join(dir, name))
42
+ .find(dir => fs.existsSync(path.join(dir, 'package.json')))
43
+ if (!found) {
44
+ throw new Error(`The ${task} task needs the optional peer dependency ${name}, which is not installed. ${installHint}`)
45
+ }
46
+ return found
47
+ }
48
+
49
+ /**
50
+ * Resolve jsdoc's own CLI entry point. jsdoc has no stable programmatic Node API for generating HTML docs (unlike
51
+ * TypeDoc's Application.bootstrap()/convert()/generateDocs()) - gulp-jsdoc3, the established gulp wrapper, is
52
+ * stale, and its own README admits jsdoc does not support piped input in the first place, a real anti-pattern for
53
+ * this project's own gulp-stream conventions. Running its real CLI via child_process is the portable way in -
54
+ * confirmed via `npm view jsdoc bin`, which reports "jsdoc.js" at the package root.
55
+ * @memberOf module:partials
56
+ * @param {string} [projectPath=process.cwd()]
57
+ * @returns {string}
58
+ */
59
+ export const resolveJsdocBin = (projectPath = process.cwd()) =>
60
+ path.join(resolvePeerDir('jsdoc', { task: 'docs', installHint: jsdocInstallHint, projectPath }), 'jsdoc.js')
61
+
62
+ /**
63
+ * Resolve a jsdoc theme package's own template directory, for jsdoc's own opts.template config key. Not
64
+ * necessarily the package's own root - clean-jsdoc-theme's own docs say to point opts.template at its `dist`
65
+ * subdirectory specifically, confirmed by reading its real README rather than assuming every theme follows the
66
+ * classic jsdoc template layout (a bare package root with its own tmpl/static folders) - clean-jsdoc-theme does
67
+ * not (it is a modern, self-contained, server-rendered theme with no such folders at its package root at all).
68
+ * @memberOf module:partials
69
+ * @param {string} name
70
+ * @param {string} [projectPath=process.cwd()]
71
+ * @returns {string}
72
+ */
73
+ export const resolveJsdocTheme = (name, projectPath = process.cwd()) => {
74
+ const packageDir = resolvePeerDir(name, { task: 'docs', installHint: themeInstallHint(name), projectPath })
75
+ return name === 'clean-jsdoc-theme' ? path.join(packageDir, 'dist') : packageDir
76
+ }
77
+
78
+ /**
79
+ * Generate the HTML documentation of a plain-JavaScript project with jsdoc, using jsdoc.base.js's own shared
80
+ * config (plugins, includePattern/excludePattern) as the starting point rather than a parallel one - the same
81
+ * base a project's own .jsdoc.conf.js has always been able to extend, just with source.include and the output
82
+ * options generated here instead of hand-maintained per project. Configure this with 'docs.from', 'docs.to',
83
+ * 'docs.index', 'docs.theme' and 'docs.title'.
84
+ * @memberOf module:partials
85
+ * @param {string} [srcPath=gulpConfig.get('docs.from')] - The directory (or glob) holding the JS source.
86
+ * @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
87
+ * @param {function(string, Array<string>): Promise<Object>} [runJsdoc] - Runs the jsdoc CLI, tests can inject a stand-in.
88
+ * @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
89
+ */
90
+ export const jsdocDocsFor = (
91
+ srcPath = gulpConfig.get('docs.from'),
92
+ outPath = gulpConfig.get('docs.to'),
93
+ runJsdoc = (bin, args) => execFileAsync(process.execPath, [bin, ...args])
94
+ ) => {
95
+ if (gulpConfig.get('docs.enabled') === false) {
96
+ return () => {}
97
+ }
98
+ return (done) => {
99
+ const generate = async () => {
100
+ const theme = gulpConfig.get('docs.theme') || 'clean-jsdoc-theme'
101
+ const themeDir = resolveJsdocTheme(theme)
102
+ const indexFile = gulpConfig.get('docs.index')
103
+ await removeDirectory(outPath)
104
+ const config = {
105
+ ...jsdocBase,
106
+ source: {
107
+ ...jsdocBase.source,
108
+ include: [path.resolve(srcPath)]
109
+ },
110
+ opts: {
111
+ destination: path.resolve(outPath),
112
+ // jsdoc does not recurse into source.include's own subdirectories by default - without this, only
113
+ // the top level of docs.from would ever be scanned. Confirmed from clean-jsdoc-theme's own README
114
+ // quick-start example, which sets this explicitly, not assumed from jsdoc's own classic defaults.
115
+ recurse: true,
116
+ template: themeDir,
117
+ ...(indexFile && fs.existsSync(indexFile) ? { readme: path.resolve(indexFile) } : {})
118
+ }
119
+ }
120
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsdocdocs-'))
121
+ try {
122
+ const configPath = path.join(workDir, 'jsdoc.conf.json')
123
+ fs.writeFileSync(configPath, JSON.stringify(config))
124
+ const bin = resolveJsdocBin()
125
+ await runJsdoc(bin, ['-c', configPath])
126
+ } finally {
127
+ await removeDirectory(workDir)
128
+ }
129
+ }
130
+ return typeof done === 'function' ? generate().then(() => done(), done) : generate()
131
+ }
132
+ }
@@ -0,0 +1,101 @@
1
+ import fs from 'fs'
2
+ import path from 'node:path'
3
+ import * as setUp from '../test-helpers/setUp.mjs'
4
+ import { jsdocDocsFor, resolveJsdocBin, resolveJsdocTheme } from './jsdocDocsFor.mjs'
5
+
6
+ setUp.setDefaults('test-jsdoc-docs-for')
7
+ const gulpConfig = setUp.gulpConfig
8
+
9
+ const write = (file, contents) => {
10
+ fs.mkdirSync(path.dirname(file), { recursive: true })
11
+ fs.writeFileSync(file, contents)
12
+ }
13
+
14
+ beforeEach(
15
+ () => setUp.beforeEach().then(() => {
16
+ const srcPath = gulpConfig.get('srcPath')
17
+ write(`${srcPath}/add.js`, '/**\n * Add two numbers.\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nconst add = (a, b) => a + b\n\nmodule.exports = add\n')
18
+ })
19
+ )
20
+
21
+ afterEach(setUp.afterEach)
22
+
23
+ describe('resolveJsdocBin / resolveJsdocTheme', () => {
24
+ test('resolves the real installed jsdoc CLI entry', () => {
25
+ const bin = resolveJsdocBin()
26
+ expect(fs.existsSync(bin)).toBe(true)
27
+ expect(path.basename(bin)).toBe('jsdoc.js')
28
+ })
29
+
30
+ test('resolves the real installed theme directory', () => {
31
+ const themeDir = resolveJsdocTheme('clean-jsdoc-theme')
32
+ expect(fs.existsSync(themeDir)).toBe(true)
33
+ })
34
+
35
+ test('throws a clear, install-hint error for a package that is not installed', () => {
36
+ expect(() => resolveJsdocTheme('not-a-real-jsdoc-theme-package')).toThrow(/npm install --save-dev/)
37
+ })
38
+ })
39
+
40
+ describe('jsdocDocsFor', () => {
41
+ const fakeRunJsdoc = calls => (bin, args) => {
42
+ calls.bin = bin
43
+ calls.args = args
44
+ calls.config = JSON.parse(fs.readFileSync(args[args.indexOf('-c') + 1], 'utf8'))
45
+ fs.mkdirSync(calls.config.opts.destination, { recursive: true })
46
+ fs.writeFileSync(`${calls.config.opts.destination}/index.html`, '<html></html>')
47
+ return Promise.resolve()
48
+ }
49
+
50
+ test('generates the documentation into the output directory, clearing what was there', async () => {
51
+ gulpConfig.set('docs.enabled', true)
52
+ const outPath = `${gulpConfig.get('srcPath')}/../docs`
53
+ write(`${outPath}/stale.html`, 'old')
54
+ const calls = {}
55
+ await jsdocDocsFor(gulpConfig.get('srcPath'), outPath, fakeRunJsdoc(calls))()
56
+ expect(fs.existsSync(`${outPath}/stale.html`)).toBe(false)
57
+ expect(fs.existsSync(`${outPath}/index.html`)).toBe(true)
58
+ expect(calls.config.opts.destination).toBe(path.resolve(outPath))
59
+ expect(calls.config.source.include).toEqual([path.resolve(gulpConfig.get('srcPath'))])
60
+ })
61
+
62
+ test('uses the configured theme as the template directory', async () => {
63
+ gulpConfig.set('docs.enabled', true)
64
+ gulpConfig.set('docs.theme', 'clean-jsdoc-theme')
65
+ const calls = {}
66
+ await jsdocDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeRunJsdoc(calls))()
67
+ expect(calls.config.opts.template).toBe(resolveJsdocTheme('clean-jsdoc-theme'))
68
+ })
69
+
70
+ test('uses the front page from docs.index when it exists, and leaves readme unset when it does not', async () => {
71
+ gulpConfig.set('docs.enabled', true)
72
+ gulpConfig.set('docs.index', `${gulpConfig.get('srcPath')}/../MAIN.md`)
73
+ write(`${gulpConfig.get('srcPath')}/../MAIN.md`, '# hello\n')
74
+ const calls = {}
75
+ await jsdocDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeRunJsdoc(calls))()
76
+ expect(calls.config.opts.readme).toBe(path.resolve(`${gulpConfig.get('srcPath')}/../MAIN.md`))
77
+
78
+ gulpConfig.set('docs.index', 'does-not-exist.md')
79
+ await jsdocDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeRunJsdoc(calls))()
80
+ expect(calls.config.opts.readme).toBeUndefined()
81
+ })
82
+
83
+ test('skips if docs.enabled is false', () => {
84
+ gulpConfig.set('docs.enabled', false)
85
+ const result = jsdocDocsFor()
86
+ expect(result).toBeInstanceOf(Function)
87
+ expect(result.length).toBe(0)
88
+ })
89
+ })
90
+
91
+ describe('jsdocDocsFor, end to end with the real jsdoc CLI and clean-jsdoc-theme', () => {
92
+ test('produces a real browsable HTML page from real JSDoc comments', async () => {
93
+ gulpConfig.set('docs.enabled', true)
94
+ const outPath = `${gulpConfig.get('srcPath')}/../docs`
95
+ await jsdocDocsFor(gulpConfig.get('srcPath'), outPath)()
96
+ // clean-jsdoc-theme nests its real output under its own route folders (global/, source/...), not flat at
97
+ // outPath's own root - confirmed by actually running it, not assumed from the classic jsdoc template shape.
98
+ const files = fs.readdirSync(outPath, { recursive: true })
99
+ expect(files.some(file => file.endsWith('.html'))).toBe(true)
100
+ }, 30000)
101
+ })
@@ -0,0 +1,10 @@
1
+ import * as gulpConfig from '../../gulp.config.mjs'
2
+
3
+ /**
4
+ * Pick which engine generates the HTML documentation: docs.method picks it explicitly; left unset (false, the
5
+ * default), it's inferred from typescript.enabled - every existing TypeScript project's config, which never
6
+ * sets method, keeps producing TypeDoc output exactly as it always has.
7
+ * @memberOf module:partials
8
+ * @returns {'typedoc'|'jsdoc'}
9
+ */
10
+ export const resolveDocsMethod = () => gulpConfig.get('docs.method') || (gulpConfig.get('typescript.enabled') ? 'typedoc' : 'jsdoc')
@@ -0,0 +1,33 @@
1
+ import * as setUp from '../test-helpers/setUp.mjs'
2
+ import { resolveDocsMethod } from './resolveDocsMethod.mjs'
3
+
4
+ setUp.setDefaults('test-resolve-docs-method')
5
+ const gulpConfig = setUp.gulpConfig
6
+
7
+ beforeEach(setUp.beforeEach)
8
+
9
+ afterEach(setUp.afterEach)
10
+
11
+ describe('resolveDocsMethod', () => {
12
+ test('infers jsdoc when typescript.enabled is false and docs.method is unset', () => {
13
+ gulpConfig.set('typescript.enabled', false)
14
+ gulpConfig.set('docs.method', false)
15
+ expect(resolveDocsMethod()).toBe('jsdoc')
16
+ })
17
+
18
+ test('infers typedoc when typescript.enabled is true and docs.method is unset', () => {
19
+ gulpConfig.set('typescript.enabled', true)
20
+ gulpConfig.set('docs.method', false)
21
+ expect(resolveDocsMethod()).toBe('typedoc')
22
+ })
23
+
24
+ test('docs.method overrides the typescript.enabled inference either way', () => {
25
+ gulpConfig.set('typescript.enabled', true)
26
+ gulpConfig.set('docs.method', 'jsdoc')
27
+ expect(resolveDocsMethod()).toBe('jsdoc')
28
+
29
+ gulpConfig.set('typescript.enabled', false)
30
+ gulpConfig.set('docs.method', 'typedoc')
31
+ expect(resolveDocsMethod()).toBe('typedoc')
32
+ })
33
+ })
@@ -16,10 +16,12 @@ export { distForSrc } from './partials/distForSrc.mjs'
16
16
  export { distSeries } from './partials/distSeries.mjs'
17
17
  export { imagesFor } from './partials/imagesFor.mjs'
18
18
  export { importModule, importPeer, requirePeer } from './partials/loadPeer.mjs'
19
+ export { jsdocDocsFor } from './partials/jsdocDocsFor.mjs'
19
20
  export { minifyFor } from './partials/minifyFor.mjs'
20
21
  export { neostandardLint } from './partials/neostandardLint.mjs'
21
22
  export { readmeTemplate } from './partials/readmeTemplate.mjs'
22
23
  export { removeDirectory } from './partials/removeDirectory.mjs'
24
+ export { resolveDocsMethod } from './partials/resolveDocsMethod.mjs'
23
25
  export { runOnChange } from './partials/runOnChange.mjs'
24
26
  export { sassFor } from './partials/sassFor.mjs'
25
27
  export { tsFor } from './partials/tsFor.mjs'
@@ -2,6 +2,6 @@ import * as partials from './partials.mjs'
2
2
 
3
3
  describe('partials', () => {
4
4
  test('is object with all partial exports', () => {
5
- expect(Object.keys(partials).length).toBe(19)
5
+ expect(Object.keys(partials).length).toBe(21)
6
6
  })
7
7
  })
package/gulp.config.mjs CHANGED
@@ -45,6 +45,15 @@ import { fileExists } from 'test-filesystem'
45
45
  * Any single configuration option is a Setting.
46
46
  * @typedef {ArrayableSetting|BooleanSetting|FlagsSetting|StringSetting} Setting
47
47
  */
48
+ /**
49
+ * One copy-files entry: a glob (or array of globs) to copy, and where to copy it.
50
+ * @typedef {Object} module:gulpConfig~CopyFilesEntry
51
+ * @property {ArrayableSetting} from - The glob pattern(s) of files to copy.
52
+ * @property {StringSetting} to - The directory to copy them into.
53
+ * @property {FlagStringSetting} base - Optional base for gulp's own src(pattern, {base}), to preserve a glob's
54
+ * own subdirectory structure in the destination instead of flattening it. false (the default) uses gulp's own
55
+ * default base behaviour.
56
+ */
48
57
  /**
49
58
  * Configurations for building the browser files.
50
59
  * @typedef {Object<string, Setting>} module:gulpConfig~BrowserConfig
@@ -119,16 +128,24 @@ import { fileExists } from 'test-filesystem'
119
128
  * @property {StringSetting} to - The directory to output the readme file in.
120
129
  */
121
130
  /**
122
- * Configurations for generating the HTML documentation of a TypeScript project (with TypeDoc, from the source).
131
+ * Configurations for generating the HTML documentation of a project's own source, with either TypeDoc (real
132
+ * TypeScript types) or jsdoc (a modern theme over jsdoc's own classic default) - see docs.method.
123
133
  * @typedef {Object<string, Setting>} module:gulpConfig~DocsConfig
124
- * @property {BooleanSetting} enabled - Toggle generating the documentation from the TypeScript source. When it is on the
134
+ * @property {BooleanSetting} enabled - Toggle generating the documentation from the source. When it is on the
125
135
  * build does this instead of generating the readme from the jsdoc comments of the compiled files.
126
- * @property {StringSetting} from - The directory holding the TypeScript source, each folder in it becomes a module.
136
+ * @property {StringSetting} from - The directory holding the source, each folder in it becomes a module (TypeDoc) or
137
+ * is scanned recursively (jsdoc).
127
138
  * @property {StringSetting} index - The markdown file used as the front page of the documentation.
128
- * @property {StringSetting} logLevel - How much TypeDoc reports: Verbose, Info, Warn, Error or None.
139
+ * @property {StringSetting} logLevel - How much TypeDoc reports: Verbose, Info, Warn, Error or None. Not used by jsdoc.
140
+ * @property {FlagStringSetting} method - 'typedoc' or 'jsdoc'. False (the default) infers it from typescript.enabled -
141
+ * 'typedoc' when on, 'jsdoc' when off - so an existing TypeScript project's config needs no change to keep working
142
+ * exactly as it already does; set this explicitly to override that inference either way.
143
+ * @property {StringSetting} theme - The jsdoc theme package to use (only meaningful when method is 'jsdoc' - TypeDoc
144
+ * keeps its own default regardless). Defaults to 'clean-jsdoc-theme'.
129
145
  * @property {StringSetting} title - The name of the documentation, defaults to the name in the package.json.
130
146
  * @property {StringSetting} to - The directory to generate the documentation in (it is cleared first).
131
- * @property {FlagStringSetting} tsconfig - The tsconfig file to read the source with, defaults to the one of typescript.config.
147
+ * @property {FlagStringSetting} tsconfig - The tsconfig file to read the source with (TypeDoc only), defaults to the
148
+ * one of typescript.config.
132
149
  */
133
150
  /**
134
151
  * Configurations to compile and copy the sass files into css.
@@ -159,6 +176,9 @@ import { fileExists } from 'test-filesystem'
159
176
  * @typedef {Object<string, Setting>} module:gulpConfig~Configurations
160
177
  * @property {BrowserConfig} browser - Browser bundling configuration group.
161
178
  * @property {ArrayableSetting} cleanPaths - The paths for directories to delete before build.
179
+ * @property {Array<CopyFilesEntry>} copyFiles - Files to copy elsewhere, run via the copyFiles task - never
180
+ * automatically as part of build, since what this copies is often the live, publicly-reachable version of
181
+ * something (e.g. a bundle), and publishing it should be a deliberate step.
162
182
  * @property {DistConfig} dist - Distribution file generation configuration group.
163
183
  * @property {DocsConfig} docs - Generate the documentation of a TypeScript project configuration group.
164
184
  * @property {FontConfig} fonts - Fonts copy configuration group.
@@ -187,6 +207,7 @@ const setDefaults = {
187
207
  to: 'browser',
188
208
  },
189
209
  cleanPaths: ['dist', 'browser'],
210
+ copyFiles: [],
190
211
  dist: {
191
212
  from: 'src/**/!(*.test).js',
192
213
  main: 'dist/main',
@@ -197,6 +218,8 @@ const setDefaults = {
197
218
  from: 'src',
198
219
  index: 'MAIN.md',
199
220
  logLevel: 'Warn',
221
+ method: false,
222
+ theme: 'clean-jsdoc-theme',
200
223
  title: '',
201
224
  to: 'docs',
202
225
  tsconfig: false,