js-build-tools 5.1.1 → 5.3.0

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 (63) hide show
  1. package/README.md +348 -284
  2. package/dist/build.js +7 -2
  3. package/dist/build.min.js +1 -1
  4. package/dist/bundle.js +32 -16
  5. package/dist/bundle.min.js +1 -1
  6. package/dist/bundleLint.js +16 -16
  7. package/dist/bundleMinify.js +13 -13
  8. package/dist/compileReadme.js +10 -10
  9. package/dist/copyFonts.js +13 -13
  10. package/dist/default.js +15 -15
  11. package/dist/dist.js +10 -10
  12. package/dist/distLint.js +16 -16
  13. package/dist/distMinify.js +13 -13
  14. package/dist/images.js +10 -10
  15. package/dist/partials/addToReadme.js +17 -17
  16. package/dist/partials/beginWatcher.js +12 -12
  17. package/dist/partials/clean.js +14 -14
  18. package/dist/partials/copyFor.js +12 -12
  19. package/dist/partials/distFor.js +21 -21
  20. package/dist/partials/distForSrc.js +11 -11
  21. package/dist/partials/distSeries.js +19 -19
  22. package/dist/partials/imagesFor.js +48 -48
  23. package/dist/partials/minifyFor.js +16 -16
  24. package/dist/partials/readmeTemplate.js +14 -14
  25. package/dist/partials/removeDirectory.js +29 -29
  26. package/dist/partials/runOnChange.js +28 -28
  27. package/dist/partials/sassFor.js +28 -28
  28. package/dist/partials/standardLint.js +75 -75
  29. package/dist/partials/tsFor.js +82 -82
  30. package/dist/partials/typeDocsFor.js +214 -0
  31. package/dist/partials.js +7 -0
  32. package/dist/partials.min.js +1 -1
  33. package/dist/sass.js +10 -10
  34. package/dist/test-helpers/setUp.js +89 -89
  35. package/dist/test-helpers/standardLintWithRealStandard.js +27 -27
  36. package/dist/testFull.js +20 -20
  37. package/dist/testHelpers.js +10 -10
  38. package/dist/testQuick.js +22 -22
  39. package/dist/typeDocs.js +14 -0
  40. package/dist/typeDocs.min.js +1 -0
  41. package/dist/typeScript.js +9 -9
  42. package/dist/utilities/dotGet.js +46 -46
  43. package/dist/utilities/dotNotate.js +46 -46
  44. package/dist/utilities/dotSet.js +37 -37
  45. package/dist/utilities/isObject.js +8 -8
  46. package/dist/utilities/strAfter.js +11 -11
  47. package/dist/utilities/strBefore.js +11 -11
  48. package/dist/watchFull.js +10 -10
  49. package/dist/watchTest.js +16 -16
  50. package/functions/build.mjs +7 -2
  51. package/functions/build.test.mjs +29 -0
  52. package/functions/bundle.mjs +20 -4
  53. package/functions/bundle.test.mjs +74 -0
  54. package/functions/partials/typeDocsFor.mjs +194 -0
  55. package/functions/partials/typeDocsFor.test.mjs +183 -0
  56. package/functions/partials.mjs +1 -0
  57. package/functions/partials.test.mjs +1 -1
  58. package/functions/typeDocs.mjs +9 -0
  59. package/functions/typeDocs.test.mjs +11 -0
  60. package/gulp.config.mjs +32 -0
  61. package/gulpfile.base.js +2 -0
  62. package/gulpfile.base.mjs +1 -0
  63. package/package.json +12 -7
package/dist/watchTest.js CHANGED
@@ -1,20 +1,20 @@
1
- 'use strict'
2
-
3
- require('core-js/modules/esnext.weak-map.delete-all.js')
4
- Object.defineProperty(exports, '__esModule', {
5
- value: true
6
- })
7
- exports.watchTest = void 0
8
- const gulpConfig = _interopRequireWildcard(require('../gulp.config.js'))
9
- const _gulp = require('gulp')
10
- const _testQuick = require('./testQuick.js')
11
- function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; let o; let i; const f = { __proto__: null, default: e }; if (e === null || typeof e !== 'object' && typeof e !== 'function') return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f) } for (const t in e) t !== 'default' && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f })(e, t) }
1
+ 'use strict'
2
+
3
+ require('core-js/modules/esnext.weak-map.delete-all.js')
4
+ Object.defineProperty(exports, '__esModule', {
5
+ value: true
6
+ })
7
+ exports.watchTest = void 0
8
+ const gulpConfig = _interopRequireWildcard(require('../gulp.config.js'))
9
+ const _gulp = require('gulp')
10
+ const _testQuick = require('./testQuick.js')
11
+ function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; let o; let i; const f = { __proto__: null, default: e }; if (e === null || typeof e !== 'object' && typeof e !== 'function') return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f) } for (const t in e) t !== 'default' && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f })(e, t) }
12
12
  /**
13
13
  * Watch for changes and run the tests.
14
14
  * @memberOf module:js-build-tools
15
15
  * @returns {*}
16
- */
17
- const watchTest = () => (0, _gulp.watch)(gulpConfig.get('test.watch'), {
18
- ignoreInitial: false
19
- }, _testQuick.testQuick)
20
- exports.watchTest = watchTest
16
+ */
17
+ const watchTest = () => (0, _gulp.watch)(gulpConfig.get('test.watch'), {
18
+ ignoreInitial: false
19
+ }, _testQuick.testQuick)
20
+ exports.watchTest = watchTest
@@ -3,6 +3,7 @@ import { bundleLint } from './bundleLint.mjs'
3
3
  import { bundleMinify } from './bundleMinify.mjs'
4
4
  import { clean, distSeries } from './partials.mjs'
5
5
  import { compileReadme } from './compileReadme.mjs'
6
+ import { typeDocs } from './typeDocs.mjs'
6
7
  import { distLint } from './distLint.mjs'
7
8
  import { distMinify } from './distMinify.mjs'
8
9
  import { parallel, series } from 'gulp'
@@ -22,7 +23,11 @@ export const build = (done = null) => {
22
23
  const distLintMinify = parallel(distLint, distMinify)
23
24
  const bundleLintMinify = parallel(bundleLint, bundleMinify)
24
25
  const buildActions = [distSeries(), distLintMinify]
25
- if (gulpConfig.get('typescript.enabled')) {
26
+ const generateDocs = gulpConfig.get('docs.enabled')
27
+ if (generateDocs) {
28
+ // The documentation is generated from the TypeScript source itself, the readme is written by hand
29
+ buildActions.push(typeDocs)
30
+ } else if (gulpConfig.get('typescript.enabled')) {
26
31
  // For ts usage, we need to run the readme on the dist directly since that is where the .js files are located
27
32
  buildActions.push(compileReadme)
28
33
  }
@@ -34,7 +39,7 @@ export const build = (done = null) => {
34
39
  const runActions = [
35
40
  series(...buildActions),
36
41
  ]
37
- if (!gulpConfig.get('typescript.enabled')) {
42
+ if (!generateDocs && !gulpConfig.get('typescript.enabled')) {
38
43
  // Since we didn't run this in series after dist because of typescript, we need to run it now. Potentially faster here.
39
44
  runActions.push(compileReadme)
40
45
  }
@@ -3,6 +3,7 @@ import * as setUp from './test-helpers/setUp.mjs'
3
3
  import { build } from './build.mjs'
4
4
  import { clean } from './partials/clean.mjs'
5
5
  import { testFull } from './testFull.mjs'
6
+ import { typeDocs } from './typeDocs.mjs'
6
7
  import { countMatches } from 'test-filesystem'
7
8
 
8
9
  // Run the real Standard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
@@ -15,6 +16,8 @@ const genericFunction = () => Promise.resolve(true)
15
16
 
16
17
  jest.mock('./partials/clean.mjs', () => ({ clean: jest.fn(genericFunction) }))
17
18
  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
+ jest.mock('./typeDocs.mjs', () => ({ typeDocs: jest.fn(genericFunction) }))
18
21
  // tsFor's real implementation loads gulp-ts-compile (a genuine ESM-only package) via a dynamic import() hidden
19
22
  // from babel's static analysis - necessary for production correctness (see tsFor.mjs's own comments), but this
20
23
  // project's current CommonJS-transform-based Jest setup can't execute a real dynamic import at all: it doesn't
@@ -625,6 +628,31 @@ describe('build', () => {
625
628
  })
626
629
  }, 30000)
627
630
 
631
+ test('when docs.enabled is set: generates the documentation from the TypeScript source instead of the readme', done => {
632
+ const srcPath = gulpConfig.get('srcPath')
633
+ const rootPath = gulpConfig.get('rootPath')
634
+ const tsConfig = `${rootPath}/tsconfig.json`
635
+ const readmePath = gulpConfig.get('readme.to')
636
+ const readmeFile = gulpConfig.get('readme.file')
637
+ fs.writeFileSync(`${srcPath}/main.ts`, mainTypescript)
638
+ fs.writeFileSync(tsConfig, tsConfigContents)
639
+ fs.rmSync(`${readmePath}/${readmeFile}`, { force: true })
640
+ typeDocs.mockClear()
641
+ gulpConfig.set('browser.enabled', false)
642
+ gulpConfig.set('typescript.config', tsConfig)
643
+ gulpConfig.set('typescript.enabled', true)
644
+ gulpConfig.set('docs.enabled', true)
645
+ expect.assertions(3)
646
+ build(() => {
647
+ gulpConfig.set('docs.enabled', false)
648
+ expect(typeDocs).toHaveBeenCalledTimes(1)
649
+ // The readme is not generated, it is written by hand
650
+ expect(fs.existsSync(`${readmePath}/${readmeFile}`)).toBe(false)
651
+ expect(testFull).toHaveBeenCalled()
652
+ done()
653
+ })
654
+ }, 30000)
655
+
628
656
  test('finishes clean before starting the tasks that write into the output folders', done => {
629
657
  const srcPath = gulpConfig.get('srcPath')
630
658
  const sourcesPath = `${srcPath}/sources`
@@ -647,6 +675,7 @@ describe('build', () => {
647
675
  gulpConfig.set('images.enabled', false)
648
676
  gulpConfig.set('sass.enabled', false)
649
677
  gulpConfig.set('typescript.enabled', false)
678
+ gulpConfig.set('docs.enabled', false)
650
679
  build(() => {
651
680
  expect(events).toEqual(['clean finished', 'testFull started'])
652
681
  done()
@@ -5,10 +5,26 @@ import source from 'vinyl-source-stream'
5
5
 
6
6
  /**
7
7
  * Starting at the distribution entry point, bundle all the files into a single file and store them in the specified output directory.
8
+ *
9
+ * Two escape hatches from browser.config.json keep an unwanted dependency (or a dependency's dependency) out of the
10
+ * bundle: 'ignore' replaces it with an empty object (safe when your code never actually calls into it - for example
11
+ * a library's default option value you always override with your own) and 'exclude' leaves it out entirely (your
12
+ * code must not require it, or provide it another way, such as a separate script tag).
8
13
  * @memberOf module:js-build-tools
9
14
  * @returns {stream.Stream}
10
15
  */
11
- export const bundle = () => browserify(gulpConfig.get('dist.main'))
12
- .bundle()
13
- .pipe(source(`${gulpConfig.get('browser.name')}.js`))
14
- .pipe(dest(gulpConfig.get('browser.to')))
16
+ export const bundle = () => {
17
+ const bundler = browserify(gulpConfig.get('dist.main'))
18
+ const ignore = gulpConfig.get('browser.ignore')
19
+ const exclude = gulpConfig.get('browser.exclude')
20
+ if (ignore && ignore.length) {
21
+ bundler.ignore(ignore)
22
+ }
23
+ if (exclude && exclude.length) {
24
+ bundler.exclude(exclude)
25
+ }
26
+ return bundler
27
+ .bundle()
28
+ .pipe(source(`${gulpConfig.get('browser.name')}.js`))
29
+ .pipe(dest(gulpConfig.get('browser.to')))
30
+ }
@@ -67,3 +67,77 @@ describe('bundle', () => {
67
67
  })
68
68
  })
69
69
  })
70
+
71
+ describe('bundle ignore / exclude', () => {
72
+ const optionalDepMarker = 'MARKER_FROM_THE_OPTIONAL_DEPENDENCY_CODE'
73
+
74
+ beforeEach(() => {
75
+ const tempDir = gulpConfig.get('rootPath')
76
+ const depDir = `${tempDir}node_modules/optional-dep`
77
+ fs.mkdirSync(depDir, { recursive: true })
78
+ fs.writeFileSync(`${depDir}/package.json`, JSON.stringify({ name: 'optional-dep', main: 'index.js' }))
79
+ fs.writeFileSync(`${depDir}/index.js`, `module.exports = '${optionalDepMarker}'\n`)
80
+ const distPath = gulpConfig.get('dist.to')
81
+ const mainFile = gulpConfig.get('dist.main')
82
+ fs.mkdirSync(distPath, { recursive: true })
83
+ // Only reached as a fallback for an option the caller always overrides, the way css-select falls back to its
84
+ // own default adapter (domutils) unless one is given - exactly the case ignore / exclude exist for.
85
+ fs.writeFileSync(`${mainFile}.js`, 'const optionalDep = require(\'optional-dep\')\n\nmodule.exports = (given) => given || optionalDep\n')
86
+ })
87
+
88
+ test('without ignore or exclude, the dependency is bundled', done => {
89
+ const destPath = gulpConfig.get('browser.to')
90
+ const bundledFile = `${destPath}/${gulpConfig.get('browser.name')}.js`
91
+ expect.assertions(1)
92
+ bundle()
93
+ .on('finish', () => {
94
+ expect(fs.readFileSync(bundledFile).toString()).toContain(optionalDepMarker)
95
+ done()
96
+ })
97
+ .on('error', error => done(error))
98
+ })
99
+
100
+ test('ignore replaces the dependency with an empty object, but the bundle still finishes', done => {
101
+ gulpConfig.set('browser.ignore', ['optional-dep'])
102
+ const destPath = gulpConfig.get('browser.to')
103
+ const bundledFile = `${destPath}/${gulpConfig.get('browser.name')}.js`
104
+ expect.assertions(2)
105
+ bundle()
106
+ .on('finish', () => {
107
+ const bundledContents = fs.readFileSync(bundledFile).toString()
108
+ // The ignored module is still in the bundle (as an empty stub, so requiring it by name still resolves) -
109
+ // it just no longer has the real code that would have pulled in the marker
110
+ expect(bundledContents).toContain('optional-dep')
111
+ expect(bundledContents).not.toContain(optionalDepMarker)
112
+ done()
113
+ })
114
+ .on('error', error => done(error))
115
+ })
116
+
117
+ test('exclude leaves the dependency out of the bundle entirely', done => {
118
+ gulpConfig.set('browser.exclude', ['optional-dep'])
119
+ const destPath = gulpConfig.get('browser.to')
120
+ const bundledFile = `${destPath}/${gulpConfig.get('browser.name')}.js`
121
+ expect.assertions(1)
122
+ bundle()
123
+ .on('finish', () => {
124
+ expect(fs.readFileSync(bundledFile).toString()).not.toContain(optionalDepMarker)
125
+ done()
126
+ })
127
+ .on('error', error => done(error))
128
+ })
129
+
130
+ test('a single string is accepted the same as a one item array', done => {
131
+ gulpConfig.set('browser.ignore', 'optional-dep')
132
+ const destPath = gulpConfig.get('browser.to')
133
+ const bundledFile = `${destPath}/${gulpConfig.get('browser.name')}.js`
134
+ expect.assertions(1)
135
+ bundle()
136
+ .on('finish', () => {
137
+ expect(fs.readFileSync(bundledFile).toString()).not.toContain(optionalDepMarker)
138
+ done()
139
+ })
140
+ .on('error', error => done(error))
141
+ })
142
+ })
143
+
@@ -0,0 +1,194 @@
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 { pathToFileURL } from 'node:url'
6
+ import * as gulpConfig from '../../gulp.config.mjs'
7
+ import { loadTypescript } from './tsFor.mjs'
8
+ import { removeDirectory } from './removeDirectory.mjs'
9
+
10
+ // TypeDoc ships ESM-only, and this project also compiles itself to a CommonJS build (see tsFor.mjs, which hides its
11
+ // own import of gulp-ts-compile the same way), so the import() is hidden from babel's CommonJS transform.
12
+ const importModule = new Function('specifier', 'return import(specifier)')
13
+
14
+ const installHint = 'Install it in your project with: npm install --save-dev typedoc'
15
+
16
+ /**
17
+ * Find the TypeDoc of the project being built (an optional peer dependency, only needed when the docs task is
18
+ * enabled) and load it, so projects which do not document TypeScript never need it installed.
19
+ * @memberOf module:partials
20
+ * @param {string} [projectPath=process.cwd()] The folder of the project (which has the package.json) being built.
21
+ * @returns {Promise<Object>} The TypeDoc module.
22
+ * @throws {Error} With install instructions when typedoc is not installed.
23
+ */
24
+ export const loadTypeDoc = async (projectPath = process.cwd()) => {
25
+ let typeDocPath
26
+ try {
27
+ typeDocPath = createRequire(path.join(projectPath, 'package.json')).resolve('typedoc')
28
+ } catch (error) {
29
+ if (error.code === 'MODULE_NOT_FOUND') {
30
+ throw new Error(`The docs task needs the optional peer dependency typedoc, which is not installed. ${installHint}`)
31
+ }
32
+ throw error
33
+ }
34
+ return importModule(pathToFileURL(typeDocPath).href)
35
+ }
36
+
37
+ const isSource = fileName => /\.(ts|tsx|mts|cts)$/.test(fileName) && !/\.d\.[cm]?ts$/.test(fileName) && !/\.(test|spec)\./.test(fileName)
38
+
39
+ const listSources = dirPath => fs.readdirSync(dirPath, { withFileTypes: true }).flatMap(entry => {
40
+ const entryPath = path.join(dirPath, entry.name)
41
+ if (entry.isDirectory()) {
42
+ return entry.name === 'node_modules' ? [] : listSources(entryPath)
43
+ }
44
+ return isSource(entry.name) ? [entryPath] : []
45
+ })
46
+
47
+ const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1')
48
+
49
+ const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(path.sep).join('/')
50
+
51
+ /**
52
+ * Describe what a source file exports: whether it has a default export, and whether it has any named exports.
53
+ * @memberOf module:partials
54
+ * @param {Object} ts - The TypeScript compiler API.
55
+ * @param {string} filePath
56
+ * @returns {{hasDefault: boolean, hasNamed: boolean}}
57
+ */
58
+ export const describeExports = (ts, filePath) => {
59
+ const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest)
60
+ let hasDefault = false
61
+ let hasNamed = false
62
+ source.statements.forEach(statement => {
63
+ if (ts.isExportAssignment(statement)) {
64
+ hasDefault = hasDefault || !statement.isExportEquals
65
+ return
66
+ }
67
+ if (ts.isExportDeclaration(statement)) {
68
+ const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null
69
+ if (names && names.some(element => element.name.text === 'default')) {
70
+ hasDefault = true
71
+ }
72
+ if (!names || names.some(element => element.name.text !== 'default')) {
73
+ hasNamed = true
74
+ }
75
+ return
76
+ }
77
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : []
78
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
79
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
80
+ hasDefault = true
81
+ } else {
82
+ hasNamed = true
83
+ }
84
+ }
85
+ })
86
+ return { hasDefault, hasNamed }
87
+ }
88
+
89
+ /**
90
+ * Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
91
+ * entry file, so the modules of the documentation mirror the folders. The default export of each source file is
92
+ * re-exported under the name of the file (the source files hold one function each), and anything else a file exports
93
+ * is re-exported as it is (that is where the types.ts files go). Index files (the barrels), tests and declaration
94
+ * files are left out. A source directory with no folders gets a single entry called index.
95
+ * @memberOf module:partials
96
+ * @param {Object} ts - The TypeScript compiler API.
97
+ * @param {string} srcDir - The directory holding the TypeScript source.
98
+ * @param {string} entryDir - The directory to write the entry files into.
99
+ * @returns {Array<string>} The paths of the entry files.
100
+ */
101
+ export const writeDocEntries = (ts, srcDir, entryDir) => {
102
+ const absoluteSrc = path.resolve(srcDir)
103
+ fs.mkdirSync(entryDir, { recursive: true })
104
+ const folders = fs.readdirSync(absoluteSrc, { withFileTypes: true })
105
+ .filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
106
+ .map(entry => ({ name: entry.name, files: listSources(path.join(absoluteSrc, entry.name)) }))
107
+ .filter(folder => folder.files.length)
108
+ const groups = folders.length
109
+ ? folders
110
+ : [{ name: 'index', files: listSources(absoluteSrc).filter(file => !/^(index|main)\./.test(path.basename(file))) }]
111
+ return groups.map(group => {
112
+ const lines = group.files
113
+ .filter(file => !/^index\./.test(path.basename(file)))
114
+ .sort()
115
+ .flatMap(file => {
116
+ const modulePath = toModulePath(file)
117
+ const { hasDefault, hasNamed } = describeExports(ts, file)
118
+ const fileName = path.basename(file).replace(/\.[cm]?tsx?$/, '')
119
+ return [
120
+ ...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []),
121
+ ...(hasNamed ? [`export * from '${modulePath}'`] : [])
122
+ ]
123
+ })
124
+ const entryPath = path.join(entryDir, `${group.name}.ts`)
125
+ fs.writeFileSync(entryPath, lines.join('\n') + '\n')
126
+ return entryPath
127
+ })
128
+ }
129
+
130
+ const projectName = () => {
131
+ try {
132
+ return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).name
133
+ } catch (error) {
134
+ return path.basename(process.cwd())
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Generate the HTML documentation of a TypeScript project with TypeDoc, straight from the TypeScript source, so the
140
+ * types (and the comments on them) are documented from where they are defined. The modules mirror the folders of the
141
+ * source, see {@link writeDocEntries}. Configure this with 'docs.from', 'docs.to', 'docs.index', 'docs.title',
142
+ * 'docs.tsconfig' and 'docs.logLevel'.
143
+ * @memberOf module:partials
144
+ * @param {string} [srcPath=gulpConfig.get('docs.from')] - The directory holding the TypeScript source.
145
+ * @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
146
+ * @param {function(string): Promise<Object>} [loadDocs] - Resolves the TypeDoc module, tests can inject a stand-in.
147
+ * @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
148
+ */
149
+ export const typeDocsFor = (
150
+ srcPath = gulpConfig.get('docs.from'),
151
+ outPath = gulpConfig.get('docs.to'),
152
+ loadDocs = loadTypeDoc
153
+ ) => {
154
+ if (gulpConfig.get('docs.enabled') === false) {
155
+ return () => {}
156
+ }
157
+ return (done) => {
158
+ const generate = async () => {
159
+ const ts = loadTypescript()
160
+ const { Application } = await loadDocs()
161
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'typedocs-'))
162
+ try {
163
+ const entryPoints = writeDocEntries(ts, srcPath, path.join(workDir, 'entries'))
164
+ // The entry files live outside the project, so the tsconfig which is used for the documentation extends the
165
+ // one of the project and adds the entries to the files it includes.
166
+ const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config')
167
+ const tsconfig = path.join(workDir, 'tsconfig.json')
168
+ fs.writeFileSync(tsconfig, JSON.stringify({
169
+ ...(projectConfig ? { extends: path.resolve(projectConfig) } : {}),
170
+ include: [path.join(workDir, 'entries', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
171
+ exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
172
+ }))
173
+ const indexFile = gulpConfig.get('docs.index')
174
+ await removeDirectory(outPath)
175
+ const app = await Application.bootstrap({
176
+ entryPoints,
177
+ tsconfig,
178
+ name: gulpConfig.get('docs.title') || projectName(),
179
+ readme: indexFile && fs.existsSync(indexFile) ? indexFile : 'none',
180
+ logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
181
+ skipErrorChecking: true
182
+ })
183
+ const project = await app.convert()
184
+ if (!project) {
185
+ throw new Error('TypeDoc could not read the TypeScript source, see the errors above.')
186
+ }
187
+ await app.generateDocs(project, outPath)
188
+ } finally {
189
+ await removeDirectory(workDir)
190
+ }
191
+ }
192
+ return typeof done === 'function' ? generate().then(() => done(), done) : generate()
193
+ }
194
+ }
@@ -0,0 +1,183 @@
1
+ import fs from 'fs'
2
+ import path from 'node:path'
3
+ import ts from 'typescript'
4
+ import * as setUp from '../test-helpers/setUp.mjs'
5
+ import { describeExports, loadTypeDoc, typeDocsFor, writeDocEntries } from './typeDocsFor.mjs'
6
+
7
+ setUp.setDefaults('test-type-docs-for')
8
+ const gulpConfig = setUp.gulpConfig
9
+
10
+ const write = (file, contents) => {
11
+ fs.mkdirSync(path.dirname(file), { recursive: true })
12
+ fs.writeFileSync(file, contents)
13
+ }
14
+
15
+ beforeEach(
16
+ () => setUp.beforeEach().then(() => {
17
+ const srcPath = gulpConfig.get('srcPath')
18
+ write(`${srcPath}/main.ts`, "import a from './point/add'\nexport default { a }\n")
19
+ write(`${srcPath}/point/add.ts`, '/** Add two numbers. */\nconst add = (a: number, b: number): number => a + b\n\nexport default add\n')
20
+ write(`${srcPath}/point/get-name.ts`, 'export default function getName (): string { return "name" }\n')
21
+ write(`${srcPath}/point/types.ts`, 'export interface Point { x: number, y: number }\nexport type Axis = keyof Point\n')
22
+ write(`${srcPath}/point/index.ts`, "import add from './add'\nexport default { add }\n")
23
+ write(`${srcPath}/point/add.test.ts`, "test('adds', () => {})\n")
24
+ write(`${srcPath}/point/add.d.ts`, 'export {}\n')
25
+ write(`${srcPath}/line/length.ts`, 'export const length = 5\nexport default length\n')
26
+ write(`${srcPath}/empty/README.md`, 'nothing to document\n')
27
+ })
28
+ )
29
+
30
+ afterEach(setUp.afterEach)
31
+
32
+ describe('describeExports', () => {
33
+ const inSrc = name => `${gulpConfig.get('srcPath')}/${name}`
34
+
35
+ test('finds a default export', () => {
36
+ expect(describeExports(ts, inSrc('point/add.ts'))).toEqual({ hasDefault: true, hasNamed: false })
37
+ })
38
+
39
+ test('finds a default function declaration', () => {
40
+ expect(describeExports(ts, inSrc('point/get-name.ts'))).toEqual({ hasDefault: true, hasNamed: false })
41
+ })
42
+
43
+ test('finds named exports (types)', () => {
44
+ expect(describeExports(ts, inSrc('point/types.ts'))).toEqual({ hasDefault: false, hasNamed: true })
45
+ })
46
+
47
+ test('finds both when a file has both', () => {
48
+ expect(describeExports(ts, inSrc('line/length.ts'))).toEqual({ hasDefault: true, hasNamed: true })
49
+ })
50
+
51
+ test('understands export lists and re-exports', () => {
52
+ write(inSrc('lists.ts'), "const a = 1\nconst b = 2\nexport { a as default, b }\nexport * from './point/types'\n")
53
+ expect(describeExports(ts, inSrc('lists.ts'))).toEqual({ hasDefault: true, hasNamed: true })
54
+ })
55
+ })
56
+
57
+ describe('writeDocEntries', () => {
58
+ test('writes one entry file for each folder which has source', () => {
59
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
60
+ const entries = writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
61
+ expect(entries.map(entry => path.basename(entry)).sort()).toEqual(['line.ts', 'point.ts'])
62
+ })
63
+
64
+ test('re-exports the default of each file by its name and the named exports as they are', () => {
65
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
66
+ writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
67
+ const absoluteSrc = path.resolve(gulpConfig.get('srcPath')).split(path.sep).join('/')
68
+ expect(fs.readFileSync(`${entryDir}/point.ts`, 'utf8').trim().split('\n')).toEqual([
69
+ `export { default as add } from '${absoluteSrc}/point/add'`,
70
+ `export { default as getName } from '${absoluteSrc}/point/get-name'`,
71
+ `export * from '${absoluteSrc}/point/types'`
72
+ ])
73
+ expect(fs.readFileSync(`${entryDir}/line.ts`, 'utf8').trim().split('\n')).toEqual([
74
+ `export { default as length } from '${absoluteSrc}/line/length'`,
75
+ `export * from '${absoluteSrc}/line/length'`
76
+ ])
77
+ })
78
+
79
+ test('leaves out index files, tests and declaration files', () => {
80
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
81
+ writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
82
+ const entry = fs.readFileSync(`${entryDir}/point.ts`, 'utf8')
83
+ expect(entry).not.toMatch(/index|\.test|add\.d/)
84
+ })
85
+
86
+ test('makes one entry called index when the source has no folders', () => {
87
+ const flat = `${gulpConfig.get('srcPath')}/../flat`
88
+ write(`${flat}/one.ts`, 'export default 1\n')
89
+ write(`${flat}/main.ts`, 'export default 2\n')
90
+ const entries = writeDocEntries(ts, flat, `${flat}/../flat-entries`)
91
+ expect(entries.map(entry => path.basename(entry))).toEqual(['index.ts'])
92
+ expect(fs.readFileSync(entries[0], 'utf8')).toMatch(/default as one/)
93
+ expect(fs.readFileSync(entries[0], 'utf8')).not.toMatch(/main/)
94
+ })
95
+ })
96
+
97
+ describe('typeDocsFor', () => {
98
+ const fakeTypeDoc = calls => () => Promise.resolve({
99
+ Application: {
100
+ bootstrap: async options => {
101
+ calls.options = options
102
+ calls.entryContents = options.entryPoints.map(entry => fs.readFileSync(entry, 'utf8'))
103
+ calls.tsconfig = JSON.parse(fs.readFileSync(options.tsconfig, 'utf8'))
104
+ return {
105
+ convert: async () => ({ name: 'project' }),
106
+ generateDocs: async (project, out) => {
107
+ calls.generated = { project, out }
108
+ fs.mkdirSync(out, { recursive: true })
109
+ fs.writeFileSync(`${out}/index.html`, '<html></html>')
110
+ }
111
+ }
112
+ }
113
+ }
114
+ })
115
+
116
+ test('generates the documentation into the output directory, clearing what was there', done => {
117
+ gulpConfig.set('docs.enabled', true)
118
+ const outPath = `${gulpConfig.get('srcPath')}/../docs`
119
+ write(`${outPath}/stale.html`, 'old')
120
+ const calls = {}
121
+ typeDocsFor(gulpConfig.get('srcPath'), outPath, fakeTypeDoc(calls))(() => {
122
+ expect(fs.existsSync(`${outPath}/stale.html`)).toBe(false)
123
+ expect(fs.existsSync(`${outPath}/index.html`)).toBe(true)
124
+ expect(calls.generated.out).toBe(outPath)
125
+ expect(calls.options.entryPoints.map(entry => path.basename(entry)).sort()).toEqual(['line.ts', 'point.ts'])
126
+ expect(calls.options.skipErrorChecking).toBe(true)
127
+ done()
128
+ })
129
+ })
130
+
131
+ test('uses the front page and the title from the config, and none when the front page is missing', async () => {
132
+ gulpConfig.set('docs.enabled', true)
133
+ gulpConfig.set('docs.title', 'my-title')
134
+ gulpConfig.set('docs.index', `${gulpConfig.get('srcPath')}/../MAIN.md`)
135
+ write(`${gulpConfig.get('srcPath')}/../MAIN.md`, '# hello\n')
136
+ const calls = {}
137
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
138
+ expect(calls.options.name).toBe('my-title')
139
+ expect(calls.options.readme).toBe(`${gulpConfig.get('srcPath')}/../MAIN.md`)
140
+ gulpConfig.set('docs.index', 'does-not-exist.md')
141
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
142
+ expect(calls.options.readme).toBe('none')
143
+ })
144
+
145
+ test('reads the source with the tsconfig of the project, adding the entries to it', async () => {
146
+ gulpConfig.set('docs.enabled', true)
147
+ gulpConfig.set('typescript.config', 'tsconfig.json')
148
+ const calls = {}
149
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
150
+ expect(calls.tsconfig.extends).toBe(path.resolve('tsconfig.json'))
151
+ expect(calls.tsconfig.include).toHaveLength(2)
152
+ expect(calls.tsconfig.exclude[0]).toMatch(/\*\*\/\*\.test\.\*$/)
153
+ gulpConfig.set('docs.tsconfig', 'other.json')
154
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
155
+ expect(calls.tsconfig.extends).toBe(path.resolve('other.json'))
156
+ gulpConfig.set('docs.tsconfig', false)
157
+ gulpConfig.set('typescript.config', false)
158
+ })
159
+
160
+ test('fails with a clear error when TypeDoc could not read the source', async () => {
161
+ gulpConfig.set('docs.enabled', true)
162
+ const failing = () => Promise.resolve({
163
+ Application: { bootstrap: async () => ({ convert: async () => undefined }) }
164
+ })
165
+ await expect(typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, failing)()).rejects.toThrow(/could not read the TypeScript source/)
166
+ })
167
+
168
+ test('skips if docs.enabled is false', () => {
169
+ gulpConfig.set('docs.enabled', false)
170
+ const result = typeDocsFor()
171
+ expect(result).toBeInstanceOf(Function)
172
+ expect(result()).toBeUndefined()
173
+ })
174
+ })
175
+
176
+ describe('loadTypeDoc', () => {
177
+ // The real import() of the ESM-only TypeDoc cannot run in this project's CommonJS-transform-based Jest setup (see
178
+ // tsFor.mjs), so loading it for real is checked by generating the documentation of a project, see the pull request.
179
+
180
+ test('explains how to install typedoc when the project does not have it', async () => {
181
+ await expect(loadTypeDoc('/')).rejects.toThrow(/optional peer dependency typedoc.*npm install --save-dev typedoc/)
182
+ })
183
+ })
@@ -22,3 +22,4 @@ export { runOnChange } from './partials/runOnChange.mjs'
22
22
  export { sassFor } from './partials/sassFor.mjs'
23
23
  export { standardLint } from './partials/standardLint.mjs'
24
24
  export { tsFor } from './partials/tsFor.mjs'
25
+ export { typeDocsFor } from './partials/typeDocsFor.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(15)
5
+ expect(Object.keys(partials).length).toBe(16)
6
6
  })
7
7
  })
@@ -0,0 +1,9 @@
1
+ import { typeDocsFor } from './partials/typeDocsFor.mjs'
2
+
3
+ /**
4
+ * Generate the HTML documentation of a TypeScript project from its source, with the modules mirroring the source
5
+ * folders. Turn it on with docs.enabled, see {@link module:partials.typeDocsFor}.
6
+ * @memberOf module:js-build-tools
7
+ * @returns {Function}
8
+ */
9
+ export const typeDocs = typeDocsFor()
@@ -0,0 +1,11 @@
1
+ import { typeDocs } from './typeDocs.mjs'
2
+ import { typeDocsFor } from './partials/typeDocsFor.mjs'
3
+
4
+ jest.mock('./partials/typeDocsFor.mjs', () => ({ typeDocsFor: jest.fn(() => () => null) }))
5
+
6
+ describe('typeDocs', () => {
7
+ test('is the task made by typeDocsFor with no parameters', () => {
8
+ expect(typeDocsFor).toHaveBeenCalledWith()
9
+ expect(typeDocs).toBeInstanceOf(Function)
10
+ })
11
+ })