js-build-tools 5.2.0 → 5.3.1

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 (66) hide show
  1. package/README.md +101 -10
  2. package/dist/build.js +63 -58
  3. package/dist/build.min.js +1 -1
  4. package/dist/bundle.js +27 -27
  5. package/dist/bundleLint.js +16 -16
  6. package/dist/bundleMinify.js +13 -13
  7. package/dist/compileReadme.js +10 -10
  8. package/dist/copyFonts.js +13 -13
  9. package/dist/default.js +15 -15
  10. package/dist/dist.js +10 -10
  11. package/dist/distLint.js +16 -16
  12. package/dist/distMinify.js +13 -13
  13. package/dist/images.js +10 -10
  14. package/dist/partials/addToReadme.js +17 -17
  15. package/dist/partials/beginWatcher.js +12 -12
  16. package/dist/partials/clean.js +14 -14
  17. package/dist/partials/copyFor.js +12 -12
  18. package/dist/partials/distFor.js +21 -21
  19. package/dist/partials/distForSrc.js +11 -11
  20. package/dist/partials/distSeries.js +19 -19
  21. package/dist/partials/imagesFor.js +7 -13
  22. package/dist/partials/loadPeer.js +99 -0
  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 +3 -5
  29. package/dist/partials/tsFor.js +8 -20
  30. package/dist/partials/typeDocsFor.js +228 -0
  31. package/dist/partials.js +26 -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/partials/imagesFor.mjs +7 -13
  53. package/functions/partials/loadPeer.mjs +75 -0
  54. package/functions/partials/loadPeer.test.mjs +58 -0
  55. package/functions/partials/standardLint.mjs +3 -5
  56. package/functions/partials/tsFor.mjs +4 -20
  57. package/functions/partials/typeDocsFor.mjs +199 -0
  58. package/functions/partials/typeDocsFor.test.mjs +204 -0
  59. package/functions/partials.mjs +2 -0
  60. package/functions/partials.test.mjs +1 -1
  61. package/functions/typeDocs.mjs +9 -0
  62. package/functions/typeDocs.test.mjs +11 -0
  63. package/gulp.config.mjs +24 -2
  64. package/gulpfile.base.js +2 -0
  65. package/gulpfile.base.mjs +1 -0
  66. package/package.json +12 -7
@@ -0,0 +1,199 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import * as gulpConfig from '../../gulp.config.mjs'
5
+ import { importPeer } from './loadPeer.mjs'
6
+ import { loadTypescript } from './tsFor.mjs'
7
+ import { removeDirectory } from './removeDirectory.mjs'
8
+
9
+ const installHint = 'Install it in your project with: npm install --save-dev typedoc'
10
+
11
+ /**
12
+ * Find the TypeDoc of the project being built (an optional peer dependency, only needed when the docs task is
13
+ * enabled) and load it, so projects which do not document TypeScript never need it installed.
14
+ * @memberOf module:partials
15
+ * @param {string} [projectPath=process.cwd()] The folder of the project (which has the package.json) being built.
16
+ * @returns {Promise<Object>} The TypeDoc module.
17
+ * @throws {Error} With install instructions when typedoc is not installed.
18
+ */
19
+ export const loadTypeDoc = (projectPath = process.cwd()) => importPeer('typedoc', { task: 'docs', installHint, projectPath })
20
+
21
+ const isSource = fileName => /\.(ts|tsx|mts|cts)$/.test(fileName) && !/\.d\.[cm]?ts$/.test(fileName) && !/\.(test|spec)\./.test(fileName)
22
+
23
+ const listSources = dirPath => fs.readdirSync(dirPath, { withFileTypes: true }).flatMap(entry => {
24
+ const entryPath = path.join(dirPath, entry.name)
25
+ if (entry.isDirectory()) {
26
+ return entry.name === 'node_modules' ? [] : listSources(entryPath)
27
+ }
28
+ return isSource(entry.name) ? [entryPath] : []
29
+ })
30
+
31
+ const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1')
32
+
33
+ const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(path.sep).join('/')
34
+
35
+ /**
36
+ * Describe what a source file exports: whether it has a default export, and whether it has any named exports.
37
+ * @memberOf module:partials
38
+ * @param {Object} ts - The TypeScript compiler API.
39
+ * @param {string} filePath
40
+ * @returns {{hasDefault: boolean, hasNamed: boolean}}
41
+ */
42
+ export const describeExports = (ts, filePath) => {
43
+ const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest)
44
+ let hasDefault = false
45
+ let hasNamed = false
46
+ source.statements.forEach(statement => {
47
+ if (ts.isExportAssignment(statement)) {
48
+ hasDefault = hasDefault || !statement.isExportEquals
49
+ return
50
+ }
51
+ if (ts.isExportDeclaration(statement)) {
52
+ const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null
53
+ if (names && names.some(element => element.name.text === 'default')) {
54
+ hasDefault = true
55
+ }
56
+ if (!names || names.some(element => element.name.text !== 'default')) {
57
+ hasNamed = true
58
+ }
59
+ return
60
+ }
61
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : []
62
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
63
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
64
+ hasDefault = true
65
+ } else {
66
+ hasNamed = true
67
+ }
68
+ }
69
+ })
70
+ return { hasDefault, hasNamed }
71
+ }
72
+
73
+ /**
74
+ * The description of a folder for its module: the first doc comment of the folder's index file, without its tags
75
+ * (@module, @file, ...), or an empty string when there is none.
76
+ * @memberOf module:partials
77
+ * @param {string} folderPath
78
+ * @returns {string}
79
+ */
80
+ export const describeFolder = folderPath => {
81
+ const indexFile = ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(name => path.join(folderPath, name)).find(file => fs.existsSync(file))
82
+ const comment = indexFile && fs.readFileSync(indexFile, 'utf8').match(/^\s*\/\*\*([\s\S]*?)\*\//)
83
+ if (!comment) {
84
+ return ''
85
+ }
86
+ const lines = comment[1].split('\n').map(line => line.replace(/^\s*\* ?/, '').trimEnd())
87
+ const tagAt = lines.findIndex(line => /^@\w+/.test(line))
88
+ return (tagAt === -1 ? lines : lines.slice(0, tagAt)).join('\n').trim()
89
+ }
90
+
91
+ /**
92
+ * Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
93
+ * entry file, so the modules of the documentation mirror the folders. The default export of each source file is
94
+ * re-exported under the name of the file (the source files hold one function each), and anything else a file exports
95
+ * is re-exported as it is (that is where the types.ts files go). Index files (the barrels), tests and declaration
96
+ * files are left out, but the first doc comment of a folder's index file becomes the description of its module. A source directory with no folders gets a single entry called index.
97
+ * @memberOf module:partials
98
+ * @param {Object} ts - The TypeScript compiler API.
99
+ * @param {string} srcDir - The directory holding the TypeScript source.
100
+ * @param {string} entryDir - The directory to write the entry files into.
101
+ * @returns {Array<string>} The paths of the entry files.
102
+ */
103
+ export const writeDocEntries = (ts, srcDir, entryDir) => {
104
+ const absoluteSrc = path.resolve(srcDir)
105
+ fs.mkdirSync(entryDir, { recursive: true })
106
+ const folders = fs.readdirSync(absoluteSrc, { withFileTypes: true })
107
+ .filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
108
+ .map(entry => ({ name: entry.name, path: path.join(absoluteSrc, entry.name), files: listSources(path.join(absoluteSrc, entry.name)) }))
109
+ .filter(folder => folder.files.length)
110
+ const groups = folders.length
111
+ ? folders
112
+ : [{ name: 'index', path: absoluteSrc, files: listSources(absoluteSrc).filter(file => !/^(index|main)\./.test(path.basename(file))) }]
113
+ return groups.map(group => {
114
+ const lines = group.files
115
+ .filter(file => !/^index\./.test(path.basename(file)))
116
+ .sort()
117
+ .flatMap(file => {
118
+ const modulePath = toModulePath(file)
119
+ const { hasDefault, hasNamed } = describeExports(ts, file)
120
+ const fileName = path.basename(file).replace(/\.[cm]?tsx?$/, '')
121
+ return [
122
+ ...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []),
123
+ ...(hasNamed ? [`export * from '${modulePath}'`] : [])
124
+ ]
125
+ })
126
+ const description = group.path ? describeFolder(group.path) : ''
127
+ // TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
128
+ const moduleComment = description ? `/**\n${description.split('\n').map(line => ` * ${line}`.trimEnd()).join('\n')}\n * @module\n */\n\n` : ''
129
+ const entryPath = path.join(entryDir, `${group.name}.ts`)
130
+ fs.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n')
131
+ return entryPath
132
+ })
133
+ }
134
+
135
+ const projectName = () => {
136
+ try {
137
+ return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).name
138
+ } catch (error) {
139
+ return path.basename(process.cwd())
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Generate the HTML documentation of a TypeScript project with TypeDoc, straight from the TypeScript source, so the
145
+ * types (and the comments on them) are documented from where they are defined. The modules mirror the folders of the
146
+ * source, see {@link writeDocEntries}. Configure this with 'docs.from', 'docs.to', 'docs.index', 'docs.title',
147
+ * 'docs.tsconfig' and 'docs.logLevel'.
148
+ * @memberOf module:partials
149
+ * @param {string} [srcPath=gulpConfig.get('docs.from')] - The directory holding the TypeScript source.
150
+ * @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
151
+ * @param {function(string): Promise<Object>} [loadDocs] - Resolves the TypeDoc module, tests can inject a stand-in.
152
+ * @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
153
+ */
154
+ export const typeDocsFor = (
155
+ srcPath = gulpConfig.get('docs.from'),
156
+ outPath = gulpConfig.get('docs.to'),
157
+ loadDocs = loadTypeDoc
158
+ ) => {
159
+ if (gulpConfig.get('docs.enabled') === false) {
160
+ return () => {}
161
+ }
162
+ return (done) => {
163
+ const generate = async () => {
164
+ const ts = loadTypescript()
165
+ const { Application } = await loadDocs()
166
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'typedocs-'))
167
+ try {
168
+ const entryPoints = writeDocEntries(ts, srcPath, path.join(workDir, 'entries'))
169
+ // The entry files live outside the project, so the tsconfig which is used for the documentation extends the
170
+ // one of the project and adds the entries to the files it includes.
171
+ const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config')
172
+ const tsconfig = path.join(workDir, 'tsconfig.json')
173
+ fs.writeFileSync(tsconfig, JSON.stringify({
174
+ ...(projectConfig ? { extends: path.resolve(projectConfig) } : {}),
175
+ include: [path.join(workDir, 'entries', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
176
+ exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
177
+ }))
178
+ const indexFile = gulpConfig.get('docs.index')
179
+ await removeDirectory(outPath)
180
+ const app = await Application.bootstrap({
181
+ entryPoints,
182
+ tsconfig,
183
+ name: gulpConfig.get('docs.title') || projectName(),
184
+ readme: indexFile && fs.existsSync(indexFile) ? indexFile : 'none',
185
+ logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
186
+ skipErrorChecking: true
187
+ })
188
+ const project = await app.convert()
189
+ if (!project) {
190
+ throw new Error('TypeDoc could not read the TypeScript source, see the errors above.')
191
+ }
192
+ await app.generateDocs(project, outPath)
193
+ } finally {
194
+ await removeDirectory(workDir)
195
+ }
196
+ }
197
+ return typeof done === 'function' ? generate().then(() => done(), done) : generate()
198
+ }
199
+ }
@@ -0,0 +1,204 @@
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, describeFolder, 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`, "/**\n * Points of a matrix and the maths on them.\n * A second line.\n * @file Not part of the description\n * @module point\n */\nimport 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('describeFolder', () => {
58
+ test('is the text of the first comment of the index file, without its tags', () => {
59
+ expect(describeFolder(`${gulpConfig.get('srcPath')}/point`)).toBe('Points of a matrix and the maths on them.\nA second line.')
60
+ })
61
+
62
+ test('is empty without an index file, or a comment in it', () => {
63
+ expect(describeFolder(`${gulpConfig.get('srcPath')}/line`)).toBe('')
64
+ write(`${gulpConfig.get('srcPath')}/bare/index.ts`, "import a from './a'\nexport default { a }\n")
65
+ expect(describeFolder(`${gulpConfig.get('srcPath')}/bare`)).toBe('')
66
+ })
67
+ })
68
+
69
+ describe('writeDocEntries', () => {
70
+ test('writes one entry file for each folder which has source', () => {
71
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
72
+ const entries = writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
73
+ expect(entries.map(entry => path.basename(entry)).sort()).toEqual(['line.ts', 'point.ts'])
74
+ })
75
+
76
+ test('re-exports the default of each file by its name and the named exports as they are', () => {
77
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
78
+ writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
79
+ const absoluteSrc = path.resolve(gulpConfig.get('srcPath')).split(path.sep).join('/')
80
+ const exportLines = file => fs.readFileSync(file, 'utf8').trim().split('\n').filter(line => line.startsWith('export'))
81
+ expect(exportLines(`${entryDir}/point.ts`)).toEqual([
82
+ `export { default as add } from '${absoluteSrc}/point/add'`,
83
+ `export { default as getName } from '${absoluteSrc}/point/get-name'`,
84
+ `export * from '${absoluteSrc}/point/types'`
85
+ ])
86
+ expect(exportLines(`${entryDir}/line.ts`)).toEqual([
87
+ `export { default as length } from '${absoluteSrc}/line/length'`,
88
+ `export * from '${absoluteSrc}/line/length'`
89
+ ])
90
+ })
91
+
92
+ test('leaves out index files, tests and declaration files', () => {
93
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
94
+ writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
95
+ const entry = fs.readFileSync(`${entryDir}/point.ts`, 'utf8')
96
+ expect(entry).not.toMatch(/from '.*(index|\.test|add\.d)/)
97
+ })
98
+
99
+ test('describes the module with the first comment of the index file of the folder', () => {
100
+ const entryDir = `${gulpConfig.get('srcPath')}/../entries`
101
+ writeDocEntries(ts, gulpConfig.get('srcPath'), entryDir)
102
+ expect(fs.readFileSync(`${entryDir}/point.ts`, 'utf8')).toMatch(/^\/\*\*\n \* Points of a matrix and the maths on them\.\n \* A second line\.\n \* @module\n \*\/\n\nexport/)
103
+ // A folder without an index file has no comment
104
+ expect(fs.readFileSync(`${entryDir}/line.ts`, 'utf8').startsWith('export')).toBe(true)
105
+ })
106
+
107
+ test('makes one entry called index when the source has no folders', () => {
108
+ const flat = `${gulpConfig.get('srcPath')}/../flat`
109
+ write(`${flat}/one.ts`, 'export default 1\n')
110
+ write(`${flat}/main.ts`, 'export default 2\n')
111
+ const entries = writeDocEntries(ts, flat, `${flat}/../flat-entries`)
112
+ expect(entries.map(entry => path.basename(entry))).toEqual(['index.ts'])
113
+ expect(fs.readFileSync(entries[0], 'utf8')).toMatch(/default as one/)
114
+ expect(fs.readFileSync(entries[0], 'utf8')).not.toMatch(/main/)
115
+ })
116
+ })
117
+
118
+ describe('typeDocsFor', () => {
119
+ const fakeTypeDoc = calls => () => Promise.resolve({
120
+ Application: {
121
+ bootstrap: async options => {
122
+ calls.options = options
123
+ calls.entryContents = options.entryPoints.map(entry => fs.readFileSync(entry, 'utf8'))
124
+ calls.tsconfig = JSON.parse(fs.readFileSync(options.tsconfig, 'utf8'))
125
+ return {
126
+ convert: async () => ({ name: 'project' }),
127
+ generateDocs: async (project, out) => {
128
+ calls.generated = { project, out }
129
+ fs.mkdirSync(out, { recursive: true })
130
+ fs.writeFileSync(`${out}/index.html`, '<html></html>')
131
+ }
132
+ }
133
+ }
134
+ }
135
+ })
136
+
137
+ test('generates the documentation into the output directory, clearing what was there', done => {
138
+ gulpConfig.set('docs.enabled', true)
139
+ const outPath = `${gulpConfig.get('srcPath')}/../docs`
140
+ write(`${outPath}/stale.html`, 'old')
141
+ const calls = {}
142
+ typeDocsFor(gulpConfig.get('srcPath'), outPath, fakeTypeDoc(calls))(() => {
143
+ expect(fs.existsSync(`${outPath}/stale.html`)).toBe(false)
144
+ expect(fs.existsSync(`${outPath}/index.html`)).toBe(true)
145
+ expect(calls.generated.out).toBe(outPath)
146
+ expect(calls.options.entryPoints.map(entry => path.basename(entry)).sort()).toEqual(['line.ts', 'point.ts'])
147
+ expect(calls.options.skipErrorChecking).toBe(true)
148
+ done()
149
+ })
150
+ })
151
+
152
+ test('uses the front page and the title from the config, and none when the front page is missing', async () => {
153
+ gulpConfig.set('docs.enabled', true)
154
+ gulpConfig.set('docs.title', 'my-title')
155
+ gulpConfig.set('docs.index', `${gulpConfig.get('srcPath')}/../MAIN.md`)
156
+ write(`${gulpConfig.get('srcPath')}/../MAIN.md`, '# hello\n')
157
+ const calls = {}
158
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
159
+ expect(calls.options.name).toBe('my-title')
160
+ expect(calls.options.readme).toBe(`${gulpConfig.get('srcPath')}/../MAIN.md`)
161
+ gulpConfig.set('docs.index', 'does-not-exist.md')
162
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
163
+ expect(calls.options.readme).toBe('none')
164
+ })
165
+
166
+ test('reads the source with the tsconfig of the project, adding the entries to it', async () => {
167
+ gulpConfig.set('docs.enabled', true)
168
+ gulpConfig.set('typescript.config', 'tsconfig.json')
169
+ const calls = {}
170
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
171
+ expect(calls.tsconfig.extends).toBe(path.resolve('tsconfig.json'))
172
+ expect(calls.tsconfig.include).toHaveLength(2)
173
+ expect(calls.tsconfig.exclude[0]).toMatch(/\*\*\/\*\.test\.\*$/)
174
+ gulpConfig.set('docs.tsconfig', 'other.json')
175
+ await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
176
+ expect(calls.tsconfig.extends).toBe(path.resolve('other.json'))
177
+ gulpConfig.set('docs.tsconfig', false)
178
+ gulpConfig.set('typescript.config', false)
179
+ })
180
+
181
+ test('fails with a clear error when TypeDoc could not read the source', async () => {
182
+ gulpConfig.set('docs.enabled', true)
183
+ const failing = () => Promise.resolve({
184
+ Application: { bootstrap: async () => ({ convert: async () => undefined }) }
185
+ })
186
+ await expect(typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, failing)()).rejects.toThrow(/could not read the TypeScript source/)
187
+ })
188
+
189
+ test('skips if docs.enabled is false', () => {
190
+ gulpConfig.set('docs.enabled', false)
191
+ const result = typeDocsFor()
192
+ expect(result).toBeInstanceOf(Function)
193
+ expect(result()).toBeUndefined()
194
+ })
195
+ })
196
+
197
+ describe('loadTypeDoc', () => {
198
+ // The real import() of the ESM-only TypeDoc cannot run in this project's CommonJS-transform-based Jest setup (see
199
+ // tsFor.mjs), so loading it for real is checked by generating the documentation of a project, see the pull request.
200
+
201
+ test('explains how to install typedoc when the project does not have it', async () => {
202
+ await expect(loadTypeDoc('/')).rejects.toThrow(/optional peer dependency typedoc.*npm install --save-dev typedoc/)
203
+ })
204
+ })
@@ -15,6 +15,7 @@ export { distFor } from './partials/distFor.mjs'
15
15
  export { distForSrc } from './partials/distForSrc.mjs'
16
16
  export { distSeries } from './partials/distSeries.mjs'
17
17
  export { imagesFor } from './partials/imagesFor.mjs'
18
+ export { importModule, importPeer, requirePeer } from './partials/loadPeer.mjs'
18
19
  export { minifyFor } from './partials/minifyFor.mjs'
19
20
  export { readmeTemplate } from './partials/readmeTemplate.mjs'
20
21
  export { removeDirectory } from './partials/removeDirectory.mjs'
@@ -22,3 +23,4 @@ export { runOnChange } from './partials/runOnChange.mjs'
22
23
  export { sassFor } from './partials/sassFor.mjs'
23
24
  export { standardLint } from './partials/standardLint.mjs'
24
25
  export { tsFor } from './partials/tsFor.mjs'
26
+ 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(19)
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
+ })
package/gulp.config.mjs CHANGED
@@ -49,8 +49,8 @@ import { fileExists } from 'test-filesystem'
49
49
  * Configurations for building the browser files.
50
50
  * @typedef {Object<string, Setting>} module:gulpConfig~BrowserConfig
51
51
  * @property {BooleanSetting} enabled - 'true' to generate browser bundled files; 'false' for node environment only
52
- * @property {StringSetting} from - The name to use for the browser-bundled output file (.js will be appended).
53
- * @property {StringSetting} name - The search pattern used for retrieving compiled distribution files.
52
+ * @property {StringSetting} from - The search pattern used for retrieving compiled distribution files.
53
+ * @property {StringSetting} name - The name to use for the browser-bundled output file (.js will be appended).
54
54
  * @property {StringSetting} to - The output directory for browser-bundled files.
55
55
  * @property {ArrayableSetting} ignore - Module names or file paths which resolve to an empty object ({}) in the
56
56
  * bundle instead of their real contents. For a dependency which is only reached because it is the default value of
@@ -91,6 +91,18 @@ import { fileExists } from 'test-filesystem'
91
91
  * @property {StringSetting} template - The file which will be pre-fixed to your README.md output.
92
92
  * @property {StringSetting} to - The directory to output the readme file in.
93
93
  */
94
+ /**
95
+ * Configurations for generating the HTML documentation of a TypeScript project (with TypeDoc, from the source).
96
+ * @typedef {Object<string, Setting>} module:gulpConfig~DocsConfig
97
+ * @property {BooleanSetting} enabled - Toggle generating the documentation from the TypeScript source. When it is on the
98
+ * build does this instead of generating the readme from the jsdoc comments of the compiled files.
99
+ * @property {StringSetting} from - The directory holding the TypeScript source, each folder in it becomes a module.
100
+ * @property {StringSetting} index - The markdown file used as the front page of the documentation.
101
+ * @property {StringSetting} logLevel - How much TypeDoc reports: Verbose, Info, Warn, Error or None.
102
+ * @property {StringSetting} title - The name of the documentation, defaults to the name in the package.json.
103
+ * @property {StringSetting} to - The directory to generate the documentation in (it is cleared first).
104
+ * @property {FlagStringSetting} tsconfig - The tsconfig file to read the source with, defaults to the one of typescript.config.
105
+ */
94
106
  /**
95
107
  * Configurations to compile and copy the sass files into css.
96
108
  * @typedef {Object<string, Setting>} module:gulpConfig~SassConfig
@@ -121,6 +133,7 @@ import { fileExists } from 'test-filesystem'
121
133
  * @property {BrowserConfig} browser - Browser bundling configuration group.
122
134
  * @property {ArrayableSetting} cleanPaths - The paths for directories to delete before build.
123
135
  * @property {DistConfig} dist - Distribution file generation configuration group.
136
+ * @property {DocsConfig} docs - Generate the documentation of a TypeScript project configuration group.
124
137
  * @property {FontConfig} fonts - Fonts copy configuration group.
125
138
  * @property {ImageConfig} images - Minify and copy the images configuration.
126
139
  * @property {ReadmeConfig} readme - Build readme files configuration.
@@ -150,6 +163,15 @@ const setDefaults = {
150
163
  main: 'dist/main',
151
164
  to: 'dist',
152
165
  },
166
+ docs: {
167
+ enabled: false,
168
+ from: 'src',
169
+ index: 'MAIN.md',
170
+ logLevel: 'Warn',
171
+ title: '',
172
+ to: 'docs',
173
+ tsconfig: false,
174
+ },
153
175
  fonts: {
154
176
  enabled: false,
155
177
  from: 'src/fonts/**/*',
package/gulpfile.base.js CHANGED
@@ -23,6 +23,7 @@ const sass = require('./dist/sass.js')
23
23
  const testFull = require('./dist/testFull.js')
24
24
  const testHelpers = require('./dist/testHelpers.js')
25
25
  const testQuick = require('./dist/testQuick.js')
26
+ const typeDocs = require('./dist/typeDocs.js')
26
27
  const typescript = require('./dist/typeScript')
27
28
  const watchFull = require('./dist/watchFull.js')
28
29
  const watchTest = require('./dist/watchTest.js')
@@ -34,6 +35,7 @@ module.exports = {
34
35
  bundleMinify,
35
36
  copyFonts,
36
37
  defaultCmd,
38
+ docs: typeDocs,
37
39
  dist,
38
40
  distLint,
39
41
  distMinify,
package/gulpfile.base.mjs CHANGED
@@ -22,6 +22,7 @@ export { sass } from './functions/sass.mjs'
22
22
  export { testFull } from './functions/testFull.mjs'
23
23
  export * as testHelpers from './functions/testHelpers.mjs'
24
24
  export { testQuick } from './functions/testQuick.mjs'
25
+ export { typeDocs as docs } from './functions/typeDocs.mjs'
25
26
  export { typeScript as typescript } from './functions/typeScript.mjs'
26
27
  export { watchFull } from './functions/watchFull.mjs'
27
28
  export { watchTest } from './functions/watchTest.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-build-tools",
3
- "version": "5.2.0",
3
+ "version": "5.3.1",
4
4
  "description": "Standardised build process with gulp.",
5
5
  "main": "gulpfile.base.js",
6
6
  "exports": {
@@ -52,9 +52,9 @@
52
52
  "@babel/preset-typescript": "^7.29.7",
53
53
  "@babel/runtime-corejs3": "^8.0.6",
54
54
  "autoprefixer": "^10.6.1",
55
- "babel-jest": "^30.4.1",
55
+ "babel-jest": "^30.5.2",
56
56
  "browserify": "^17.0.1",
57
- "core-js": "^3.49.0",
57
+ "core-js": "^3.50.0",
58
58
  "cssnano": "^6.1.2",
59
59
  "glob": "^13.0.6",
60
60
  "gulp": "^5.0.1",
@@ -63,27 +63,31 @@
63
63
  "gulp-postcss": "^10.0.0",
64
64
  "gulp-rename": "^2.1.0",
65
65
  "gulp-sass": "^6.0.1",
66
- "gulp-ts-compile": "^1.0.0",
66
+ "gulp-ts-compile": "^1.0.1",
67
67
  "gulp-uglify-es": "^3.0.0",
68
68
  "jest": "^30.5.2",
69
- "jest-environment-jsdom": "^30.4.1",
69
+ "jest-environment-jsdom": "^30.5.2",
70
70
  "jsdoc-to-markdown": "^9.1.3",
71
71
  "postcss": "^8.5.28",
72
72
  "regenerator-runtime": "^0.14.1",
73
- "sass": "^1.101.0",
73
+ "sass": "^1.105.0",
74
74
  "standard": "^17.1.2",
75
- "test-filesystem": "^1.5.0",
75
+ "test-filesystem": "^1.5.1",
76
76
  "through2": "^4.0.2",
77
77
  "vinyl-source-stream": "^2.0.0"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "gulp-imagemin": ">=9.0.0",
81
+ "typedoc": ">=0.28.0",
81
82
  "typescript": ">=5.0.0 <7.0.0"
82
83
  },
83
84
  "peerDependenciesMeta": {
84
85
  "gulp-imagemin": {
85
86
  "optional": true
86
87
  },
88
+ "typedoc": {
89
+ "optional": true
90
+ },
87
91
  "typescript": {
88
92
  "optional": true
89
93
  }
@@ -101,6 +105,7 @@
101
105
  "funding": "https://www.paypal.com/donate/?business=LRYR6SZRWKXVY&no_recurring=0&item_name=Let%27s+build+together+today+to+simplify+our+jobs+tomorrow.&currency_code=CAD",
102
106
  "devDependencies": {
103
107
  "gulp-imagemin": "github:jheagle/gulp-imagemin#common-js-compatibility",
108
+ "typedoc": "^0.28.20",
104
109
  "typescript": "^6.0.3"
105
110
  }
106
111
  }