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.
- package/README.md +348 -284
- package/dist/build.js +7 -2
- package/dist/build.min.js +1 -1
- package/dist/bundle.js +32 -16
- package/dist/bundle.min.js +1 -1
- package/dist/bundleLint.js +16 -16
- package/dist/bundleMinify.js +13 -13
- package/dist/compileReadme.js +10 -10
- package/dist/copyFonts.js +13 -13
- package/dist/default.js +15 -15
- package/dist/dist.js +10 -10
- package/dist/distLint.js +16 -16
- package/dist/distMinify.js +13 -13
- package/dist/images.js +10 -10
- package/dist/partials/addToReadme.js +17 -17
- package/dist/partials/beginWatcher.js +12 -12
- package/dist/partials/clean.js +14 -14
- package/dist/partials/copyFor.js +12 -12
- package/dist/partials/distFor.js +21 -21
- package/dist/partials/distForSrc.js +11 -11
- package/dist/partials/distSeries.js +19 -19
- package/dist/partials/imagesFor.js +48 -48
- package/dist/partials/minifyFor.js +16 -16
- package/dist/partials/readmeTemplate.js +14 -14
- package/dist/partials/removeDirectory.js +29 -29
- package/dist/partials/runOnChange.js +28 -28
- package/dist/partials/sassFor.js +28 -28
- package/dist/partials/standardLint.js +75 -75
- package/dist/partials/tsFor.js +82 -82
- package/dist/partials/typeDocsFor.js +214 -0
- package/dist/partials.js +7 -0
- package/dist/partials.min.js +1 -1
- package/dist/sass.js +10 -10
- package/dist/test-helpers/setUp.js +89 -89
- package/dist/test-helpers/standardLintWithRealStandard.js +27 -27
- package/dist/testFull.js +20 -20
- package/dist/testHelpers.js +10 -10
- package/dist/testQuick.js +22 -22
- package/dist/typeDocs.js +14 -0
- package/dist/typeDocs.min.js +1 -0
- package/dist/typeScript.js +9 -9
- package/dist/utilities/dotGet.js +46 -46
- package/dist/utilities/dotNotate.js +46 -46
- package/dist/utilities/dotSet.js +37 -37
- package/dist/utilities/isObject.js +8 -8
- package/dist/utilities/strAfter.js +11 -11
- package/dist/utilities/strBefore.js +11 -11
- package/dist/watchFull.js +10 -10
- package/dist/watchTest.js +16 -16
- package/functions/build.mjs +7 -2
- package/functions/build.test.mjs +29 -0
- package/functions/bundle.mjs +20 -4
- package/functions/bundle.test.mjs +74 -0
- package/functions/partials/typeDocsFor.mjs +194 -0
- package/functions/partials/typeDocsFor.test.mjs +183 -0
- package/functions/partials.mjs +1 -0
- package/functions/partials.test.mjs +1 -1
- package/functions/typeDocs.mjs +9 -0
- package/functions/typeDocs.test.mjs +11 -0
- package/gulp.config.mjs +32 -0
- package/gulpfile.base.js +2 -0
- package/gulpfile.base.mjs +1 -0
- 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
|
package/functions/build.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/functions/build.test.mjs
CHANGED
|
@@ -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()
|
package/functions/bundle.mjs
CHANGED
|
@@ -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 = () =>
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
})
|
package/functions/partials.mjs
CHANGED
|
@@ -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'
|
|
@@ -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
|
+
})
|