js-build-tools 6.5.0 → 6.7.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 (169) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +762 -692
  3. package/babel.config.js +28 -28
  4. package/dist/build.js +75 -75
  5. package/dist/bundle.js +19 -19
  6. package/dist/bundleMinify.js +7 -7
  7. package/dist/compileReadme.js +4 -4
  8. package/dist/copyFiles.js +17 -17
  9. package/dist/copyFonts.js +4 -4
  10. package/dist/default.js +5 -5
  11. package/dist/dist.js +4 -4
  12. package/dist/distMinify.js +4 -4
  13. package/dist/docs.js +7 -7
  14. package/dist/images.js +4 -4
  15. package/dist/jsdocDocs.js +6 -6
  16. package/dist/lint.js +16 -16
  17. package/dist/partials/addToReadme.js +9 -9
  18. package/dist/partials/beginWatcher.js +4 -4
  19. package/dist/partials/clean.js +5 -5
  20. package/dist/partials/copyFor.js +9 -9
  21. package/dist/partials/distFor.js +6 -6
  22. package/dist/partials/distForSrc.js +5 -5
  23. package/dist/partials/distSeries.js +7 -7
  24. package/dist/partials/imagesFor.js +93 -56
  25. package/dist/partials/jsdocDocsFor.js +46 -46
  26. package/dist/partials/lintSource.js +51 -51
  27. package/dist/partials/loadPeer.js +99 -99
  28. package/dist/partials/loadPlaywright.js +14 -14
  29. package/dist/partials/minifyFor.js +20 -20
  30. package/dist/partials/neostandardLint.js +175 -175
  31. package/dist/partials/readmeTemplate.js +4 -4
  32. package/dist/partials/removeDirectory.js +11 -11
  33. package/dist/partials/resolveDocsMethod.js +6 -6
  34. package/dist/partials/runOnChange.js +31 -31
  35. package/dist/partials/sassFor.js +6 -6
  36. package/dist/partials/serveStatic.js +9 -9
  37. package/dist/partials/tsFor.js +32 -32
  38. package/dist/partials/typeDocsFor.js +57 -51
  39. package/dist/partials.js +176 -176
  40. package/dist/sourceLint.js +18 -18
  41. package/dist/test-helpers/isolateGulpCache.js +24 -24
  42. package/dist/test-helpers/setUp.js +4 -4
  43. package/dist/testFull.js +5 -5
  44. package/dist/testQuick.js +5 -5
  45. package/dist/typeDocs.js +5 -5
  46. package/dist/typeScript.js +4 -4
  47. package/dist/utilities/convertMjsFilesForDocs.js +12 -12
  48. package/dist/utilities/dotGet.js +54 -54
  49. package/dist/utilities/dotNotate.js +79 -79
  50. package/dist/utilities/dotSet.js +45 -45
  51. package/dist/utilities/esmNamedExportsToCjs.js +15 -15
  52. package/dist/utilities/getBundleConfigs.js +9 -9
  53. package/dist/utilities/isObject.js +6 -6
  54. package/dist/utilities/strAfter.js +7 -7
  55. package/dist/utilities/strBefore.js +7 -7
  56. package/dist/watchFull.js +4 -4
  57. package/dist/watchTest.js +4 -4
  58. package/functions/build.mjs +72 -72
  59. package/functions/build.test.mjs +763 -759
  60. package/functions/bundle.mjs +44 -44
  61. package/functions/bundle.test.mjs +219 -219
  62. package/functions/bundleMinify.mjs +16 -16
  63. package/functions/bundleMinify.test.mjs +37 -37
  64. package/functions/compileReadme.mjs +9 -9
  65. package/functions/compileReadme.test.mjs +18 -18
  66. package/functions/copyFiles.mjs +32 -32
  67. package/functions/copyFiles.test.mjs +78 -78
  68. package/functions/copyFonts.mjs +9 -9
  69. package/functions/copyFonts.test.mjs +32 -32
  70. package/functions/default.mjs +12 -12
  71. package/functions/default.test.mjs +33 -33
  72. package/functions/dist.mjs +8 -8
  73. package/functions/dist.test.mjs +44 -44
  74. package/functions/distMinify.mjs +9 -9
  75. package/functions/distMinify.test.mjs +15 -15
  76. package/functions/docs.mjs +13 -13
  77. package/functions/docs.test.mjs +45 -45
  78. package/functions/gulpConfigEntries.test.mjs +23 -23
  79. package/functions/images.mjs +8 -8
  80. package/functions/images.test.mjs +142 -142
  81. package/functions/jsdocDocs.mjs +10 -10
  82. package/functions/jsdocEsmExports.test.mjs +24 -24
  83. package/functions/lint.mjs +9 -9
  84. package/functions/partials/addToReadme.mjs +25 -25
  85. package/functions/partials/addToReadme.test.mjs +34 -34
  86. package/functions/partials/beginWatcher.mjs +9 -9
  87. package/functions/partials/beginWatcher.test.mjs +19 -19
  88. package/functions/partials/clean.mjs +13 -13
  89. package/functions/partials/clean.test.mjs +22 -22
  90. package/functions/partials/copyFor.mjs +15 -15
  91. package/functions/partials/copyFor.test.mjs +53 -53
  92. package/functions/partials/distFor.mjs +22 -22
  93. package/functions/partials/distFor.test.mjs +195 -195
  94. package/functions/partials/distForSrc.mjs +11 -11
  95. package/functions/partials/distForSrc.test.mjs +18 -18
  96. package/functions/partials/distSeries.mjs +21 -21
  97. package/functions/partials/distSeries.test.mjs +34 -34
  98. package/functions/partials/imagesFor.mjs +83 -50
  99. package/functions/partials/imagesFor.test.mjs +287 -229
  100. package/functions/partials/jsdocDocsFor.mjs +163 -152
  101. package/functions/partials/jsdocDocsFor.test.mjs +182 -152
  102. package/functions/partials/lintSource.mjs +41 -41
  103. package/functions/partials/loadPeer.mjs +76 -76
  104. package/functions/partials/loadPeer.test.mjs +58 -58
  105. package/functions/partials/loadPlaywright.mjs +23 -23
  106. package/functions/partials/loadPlaywright.test.mjs +70 -70
  107. package/functions/partials/minifyFor.mjs +15 -15
  108. package/functions/partials/minifyFor.test.mjs +49 -49
  109. package/functions/partials/neostandardLint.mjs +142 -142
  110. package/functions/partials/neostandardLint.test.mjs +131 -131
  111. package/functions/partials/readmeTemplate.mjs +12 -12
  112. package/functions/partials/readmeTemplate.test.mjs +34 -34
  113. package/functions/partials/removeDirectory.mjs +34 -34
  114. package/functions/partials/removeDirectory.test.mjs +72 -72
  115. package/functions/partials/resolveDocsMethod.mjs +10 -10
  116. package/functions/partials/resolveDocsMethod.test.mjs +33 -33
  117. package/functions/partials/runOnChange.mjs +54 -54
  118. package/functions/partials/runOnChange.test.mjs +95 -95
  119. package/functions/partials/sassFor.mjs +25 -25
  120. package/functions/partials/sassFor.test.mjs +81 -81
  121. package/functions/partials/serveStatic.mjs +47 -47
  122. package/functions/partials/serveStatic.test.mjs +71 -71
  123. package/functions/partials/tsFor.mjs +92 -92
  124. package/functions/partials/tsFor.test.mjs +88 -88
  125. package/functions/partials/typeDocsFor.mjs +231 -227
  126. package/functions/partials/typeDocsFor.test.mjs +263 -252
  127. package/functions/partials.mjs +31 -31
  128. package/functions/partials.test.mjs +7 -7
  129. package/functions/sass.mjs +4 -4
  130. package/functions/sass.test.mjs +31 -31
  131. package/functions/sourceLint.mjs +13 -13
  132. package/functions/sourceLint.test.mjs +166 -166
  133. package/functions/test-helpers/isolateGulpCache.mjs +16 -16
  134. package/functions/test-helpers/neostandardLintWithRealNeostandard.mjs +40 -40
  135. package/functions/test-helpers/setUp.mjs +100 -100
  136. package/functions/test-helpers/setUp.test.mjs +19 -19
  137. package/functions/testFull.mjs +17 -17
  138. package/functions/testFull.test.mjs +17 -17
  139. package/functions/testHelpers.mjs +10 -10
  140. package/functions/testHelpers.test.mjs +7 -7
  141. package/functions/testQuick.mjs +17 -17
  142. package/functions/testQuick.test.mjs +17 -17
  143. package/functions/typeDocs.mjs +9 -9
  144. package/functions/typeDocs.test.mjs +11 -11
  145. package/functions/typeScript.mjs +8 -8
  146. package/functions/typeScript.test.mjs +11 -11
  147. package/functions/utilities/convertMjsFilesForDocs.mjs +34 -34
  148. package/functions/utilities/convertMjsFilesForDocs.test.mjs +58 -58
  149. package/functions/utilities/dotGet.mjs +49 -49
  150. package/functions/utilities/dotNotate.mjs +73 -73
  151. package/functions/utilities/dotNotate.test.mjs +41 -41
  152. package/functions/utilities/dotSet.mjs +40 -40
  153. package/functions/utilities/esmNamedExportsToCjs.mjs +30 -30
  154. package/functions/utilities/esmNamedExportsToCjs.test.mjs +47 -47
  155. package/functions/utilities/getBundleConfigs.mjs +30 -30
  156. package/functions/utilities/isObject.mjs +8 -8
  157. package/functions/utilities/strAfter.mjs +12 -12
  158. package/functions/utilities/strBefore.mjs +12 -12
  159. package/functions/watchFull.mjs +9 -9
  160. package/functions/watchFull.test.mjs +41 -41
  161. package/functions/watchTest.mjs +10 -10
  162. package/functions/watchTest.test.mjs +49 -49
  163. package/gulp.config.js +4 -4
  164. package/gulp.config.mjs +344 -327
  165. package/gulpfile.base.js +51 -51
  166. package/gulpfile.base.mjs +29 -29
  167. package/jsdoc.base.js +16 -16
  168. package/jsdoc.esmExports.js +17 -17
  169. package/package.json +146 -145
@@ -1,88 +1,88 @@
1
- /* eslint-disable no-template-curly-in-string -- fixture source code, the ${...} is meant to stay literal */
2
- import fs from 'fs'
3
- import { Transform, PassThrough } from 'node:stream'
4
- import ts from 'typescript'
5
- import File from 'vinyl'
6
- import * as setUp from '../test-helpers/setUp.mjs'
7
- import { tsFor, loadTypescript } from './tsFor.mjs'
8
- import { countMatches } from 'test-filesystem'
9
-
10
- setUp.setDefaults('test-ts-for')
11
- const gulpConfig = setUp.gulpConfig
12
-
13
- const rawContents = 'export function sayHello(name: string) {\n' +
14
- ' return `Hello from ${name}`;\n' +
15
- '}'
16
-
17
- // tsFor's real implementation loads gulp-ts-compile (a genuine ESM-only package) via a dynamic import() hidden
18
- // from babel's static analysis - necessary for production correctness (see tsFor.mjs's own comments), but not
19
- // something this project's current CommonJS-transform-based Jest setup can execute without a much bigger
20
- // migration to Jest's native ESM support. This fake stands in for it here, doing real (not mocked-away)
21
- // TypeScript transpilation via `ts.transpileModule` so this test still exercises genuine compile behavior -
22
- // gulp-ts-compile's own test suite separately covers its whole-program/cross-file-import compile behavior.
23
- const fakeLoadTsCompile = () => Promise.resolve((options) => {
24
- const transform = new Transform({
25
- objectMode: true,
26
- transform (file, enc, callback) {
27
- const { outputText } = ts.transpileModule(file.contents.toString(), { compilerOptions: options })
28
- jsStream.push(new File({ cwd: file.cwd, base: file.base, path: file.path.replace(/\.ts$/, '.js'), contents: Buffer.from(outputText) }))
29
- callback()
30
- },
31
- flush (callback) {
32
- jsStream.end()
33
- dtsStream.end()
34
- callback()
35
- }
36
- })
37
- const jsStream = new PassThrough({ objectMode: true })
38
- const dtsStream = new PassThrough({ objectMode: true })
39
- transform.js = jsStream
40
- transform.dts = dtsStream
41
- return transform
42
- })
43
-
44
- beforeEach(
45
- () => setUp.beforeEach()
46
- .then(
47
- async () => {
48
- const srcPath = gulpConfig.get('srcPath')
49
- return fs.writeFileSync(`${srcPath}/typeScript.ts`, rawContents)
50
- }
51
- )
52
- )
53
-
54
- afterEach(setUp.afterEach)
55
-
56
- describe('tsFor', () => {
57
- test('copies the src directory and compiles it into the dist directory', done => {
58
- gulpConfig.set('typescript.enabled', true)
59
- const distPath = gulpConfig.get('typescript.to')
60
- const srcPath = gulpConfig.get('srcPath')
61
- const srcFile = `${srcPath}/typeScript.ts`
62
- expect.assertions(3)
63
- const oldContents = fs.readFileSync(srcFile).toString()
64
- expect(countMatches(oldContents, 'sayHello(name: string)')).toEqual(1)
65
- tsFor(srcFile, distPath, fakeLoadTsCompile)(() => {
66
- expect(fs.existsSync(distPath)).toBeTruthy()
67
- const compiledContents = fs.readFileSync(`${distPath}/typeScript.js`).toString()
68
- expect(countMatches(compiledContents, 'sayHello(name)')).toEqual(1)
69
- done()
70
- })
71
- }, 60000)
72
-
73
- test('skips if typescript.enabled is false', () => {
74
- gulpConfig.set('typescript.enabled', false)
75
- const result = tsFor()
76
- expect(result).toBeInstanceOf(Function)
77
- })
78
- })
79
-
80
- describe('loadTypescript', () => {
81
- test('loads the typescript of the project being built', () => {
82
- expect(loadTypescript().version).toBe(ts.version)
83
- })
84
-
85
- test('explains how to install typescript when the project does not have it', () => {
86
- expect(() => loadTypescript('/')).toThrow(/optional peer dependency typescript.*npm install --save-dev typescript/)
87
- })
88
- })
1
+ /* eslint-disable no-template-curly-in-string -- fixture source code, the ${...} is meant to stay literal */
2
+ import fs from 'fs'
3
+ import { Transform, PassThrough } from 'node:stream'
4
+ import ts from 'typescript'
5
+ import File from 'vinyl'
6
+ import * as setUp from '../test-helpers/setUp.mjs'
7
+ import { tsFor, loadTypescript } from './tsFor.mjs'
8
+ import { countMatches } from 'test-filesystem'
9
+
10
+ setUp.setDefaults('test-ts-for')
11
+ const gulpConfig = setUp.gulpConfig
12
+
13
+ const rawContents = 'export function sayHello(name: string) {\n' +
14
+ ' return `Hello from ${name}`;\n' +
15
+ '}'
16
+
17
+ // tsFor's real implementation loads gulp-ts-compile (a genuine ESM-only package) via a dynamic import() hidden
18
+ // from babel's static analysis - necessary for production correctness (see tsFor.mjs's own comments), but not
19
+ // something this project's current CommonJS-transform-based Jest setup can execute without a much bigger
20
+ // migration to Jest's native ESM support. This fake stands in for it here, doing real (not mocked-away)
21
+ // TypeScript transpilation via `ts.transpileModule` so this test still exercises genuine compile behavior -
22
+ // gulp-ts-compile's own test suite separately covers its whole-program/cross-file-import compile behavior.
23
+ const fakeLoadTsCompile = () => Promise.resolve((options) => {
24
+ const transform = new Transform({
25
+ objectMode: true,
26
+ transform (file, enc, callback) {
27
+ const { outputText } = ts.transpileModule(file.contents.toString(), { compilerOptions: options })
28
+ jsStream.push(new File({ cwd: file.cwd, base: file.base, path: file.path.replace(/\.ts$/, '.js'), contents: Buffer.from(outputText) }))
29
+ callback()
30
+ },
31
+ flush (callback) {
32
+ jsStream.end()
33
+ dtsStream.end()
34
+ callback()
35
+ }
36
+ })
37
+ const jsStream = new PassThrough({ objectMode: true })
38
+ const dtsStream = new PassThrough({ objectMode: true })
39
+ transform.js = jsStream
40
+ transform.dts = dtsStream
41
+ return transform
42
+ })
43
+
44
+ beforeEach(
45
+ () => setUp.beforeEach()
46
+ .then(
47
+ async () => {
48
+ const srcPath = gulpConfig.get('srcPath')
49
+ return fs.writeFileSync(`${srcPath}/typeScript.ts`, rawContents)
50
+ }
51
+ )
52
+ )
53
+
54
+ afterEach(setUp.afterEach)
55
+
56
+ describe('tsFor', () => {
57
+ test('copies the src directory and compiles it into the dist directory', done => {
58
+ gulpConfig.set('typescript.enabled', true)
59
+ const distPath = gulpConfig.get('typescript.to')
60
+ const srcPath = gulpConfig.get('srcPath')
61
+ const srcFile = `${srcPath}/typeScript.ts`
62
+ expect.assertions(3)
63
+ const oldContents = fs.readFileSync(srcFile).toString()
64
+ expect(countMatches(oldContents, 'sayHello(name: string)')).toEqual(1)
65
+ tsFor(srcFile, distPath, fakeLoadTsCompile)(() => {
66
+ expect(fs.existsSync(distPath)).toBeTruthy()
67
+ const compiledContents = fs.readFileSync(`${distPath}/typeScript.js`).toString()
68
+ expect(countMatches(compiledContents, 'sayHello(name)')).toEqual(1)
69
+ done()
70
+ })
71
+ }, 60000)
72
+
73
+ test('skips if typescript.enabled is false', () => {
74
+ gulpConfig.set('typescript.enabled', false)
75
+ const result = tsFor()
76
+ expect(result).toBeInstanceOf(Function)
77
+ })
78
+ })
79
+
80
+ describe('loadTypescript', () => {
81
+ test('loads the typescript of the project being built', () => {
82
+ expect(loadTypescript().version).toBe(ts.version)
83
+ })
84
+
85
+ test('explains how to install typescript when the project does not have it', () => {
86
+ expect(() => loadTypescript('/')).toThrow(/optional peer dependency typescript.*npm install --save-dev typescript/)
87
+ })
88
+ })
@@ -1,227 +1,231 @@
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 })
24
- .filter(entry => entry.isFile() && isSource(entry.name))
25
- .map(entry => path.join(dirPath, entry.name))
26
-
27
- // Every folder below dirPath (at any depth), named by its path from dirPath, with the source files directly in it
28
- const listFolders = (dirPath, prefix = '') => fs.readdirSync(dirPath, { withFileTypes: true })
29
- .filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
30
- .flatMap(entry => {
31
- const folderPath = path.join(dirPath, entry.name)
32
- const name = `${prefix}${entry.name}`
33
- return [{ name, path: folderPath, files: listSources(folderPath) }, ...listFolders(folderPath, `${name}/`)]
34
- })
35
-
36
- const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1')
37
-
38
- const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(path.sep).join('/')
39
-
40
- /**
41
- * Describe what a source file exports: whether it has a default export, and whether it has any named exports.
42
- * @memberOf module:partials
43
- * @param {Object} ts - The TypeScript compiler API.
44
- * @param {string} filePath
45
- * @returns {{hasDefault: boolean, hasNamed: boolean}}
46
- */
47
- export const describeExports = (ts, filePath) => {
48
- const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest)
49
- let hasDefault = false
50
- let hasNamed = false
51
- source.statements.forEach(statement => {
52
- if (ts.isExportAssignment(statement)) {
53
- hasDefault = hasDefault || !statement.isExportEquals
54
- return
55
- }
56
- if (ts.isExportDeclaration(statement)) {
57
- const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null
58
- if (names && names.some(element => element.name.text === 'default')) {
59
- hasDefault = true
60
- }
61
- if (!names || names.some(element => element.name.text !== 'default')) {
62
- hasNamed = true
63
- }
64
- return
65
- }
66
- const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : []
67
- if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
68
- if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
69
- hasDefault = true
70
- } else {
71
- hasNamed = true
72
- }
73
- }
74
- })
75
- return { hasDefault, hasNamed }
76
- }
77
-
78
- /**
79
- * The description of a folder for its module: the first doc comment of the folder's index file, without its tags
80
- * (@module, @file, ...), or an empty string when there is none.
81
- * @memberOf module:partials
82
- * @param {string} folderPath
83
- * @returns {string}
84
- */
85
- export const describeFolder = folderPath => {
86
- const indexFile = ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(name => path.join(folderPath, name)).find(file => fs.existsSync(file))
87
- return indexFile ? describeFile(indexFile) : ''
88
- }
89
-
90
- /**
91
- * The description of a source file for its module: its first doc comment without the tags (@module, @file, ...), when
92
- * that comment is a header (it is followed by an import or export, so it does not document a declaration itself), or
93
- * an empty string.
94
- * @memberOf module:partials
95
- * @param {string} filePath
96
- * @returns {string}
97
- */
98
- export const describeFile = filePath => {
99
- const comment = fs.readFileSync(filePath, 'utf8').match(/^\s*\/\*\*([\s\S]*?)\*\/\s*(import\b|export\s+(\*|\{[^}]*\}\s+from)|$)/)
100
- if (!comment) {
101
- return ''
102
- }
103
- const lines = comment[1].split('\n').map(line => line.replace(/^\s*\* ?/, '').trimEnd())
104
- const tagAt = lines.findIndex(line => /^@\w+/.test(line))
105
- return (tagAt === -1 ? lines : lines.slice(0, tagAt)).join('\n').trim()
106
- }
107
-
108
- /**
109
- * Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
110
- * entry file, so the modules of the documentation mirror the folders. Nested folders are modules of their own, named
111
- * by their path (utilities/parsers), and a folder's module holds only the files directly in it. The default export of each source file is
112
- * re-exported under the name of the file (the source files hold one function each), and anything else a file exports
113
- * is re-exported as it is (that is where the types.ts files go). Index files (the barrels), tests and declaration
114
- * files are left out, but the first doc comment of a folder's index file becomes the description of its module.
115
- * A file which sits beside the folders (not an index, main or browser file) is a module of its own, described by its header
116
- * comment. A source directory with no folders gets a single entry called index.
117
- * @memberOf module:partials
118
- * @param {Object} ts - The TypeScript compiler API.
119
- * @param {string} srcDir - The directory holding the TypeScript source.
120
- * @param {string} entryDir - The directory to write the entry files into.
121
- * @returns {Array<string>} The paths of the entry files.
122
- */
123
- export const writeDocEntries = (ts, srcDir, entryDir) => {
124
- const absoluteSrc = path.resolve(srcDir)
125
- fs.mkdirSync(entryDir, { recursive: true })
126
- const folders = listFolders(absoluteSrc).filter(folder => folder.files.some(file => !/^index\./.test(path.basename(file))))
127
- const rootFiles = listSources(absoluteSrc).filter(file => !/^(index|main|browser)\./.test(path.basename(file)))
128
- // Folders make modules, and so do the files which sit beside them (except the barrels: index, main and browser, the
129
- // browser bundle's entry). A source directory with no folders gets a single module called index which holds its files.
130
- const groups = folders.length
131
- ? [
132
- ...folders,
133
- ...rootFiles
134
- .map(file => ({ name: toIdentifier(path.basename(file).replace(/\.[cm]?tsx?$/, '')), files: [file], description: describeFile(file) }))
135
- .filter(group => !folders.some(folder => folder.name === group.name))
136
- ]
137
- : [{ name: 'index', path: absoluteSrc, files: rootFiles }]
138
- return groups.map(group => {
139
- const lines = group.files
140
- .filter(file => !/^index\./.test(path.basename(file)))
141
- .sort()
142
- .flatMap(file => {
143
- const modulePath = toModulePath(file)
144
- const { hasDefault, hasNamed } = describeExports(ts, file)
145
- const fileName = path.basename(file).replace(/\.[cm]?tsx?$/, '')
146
- return [
147
- ...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []),
148
- ...(hasNamed ? [`export * from '${modulePath}'`] : [])
149
- ]
150
- })
151
- const description = group.description ?? (group.path ? describeFolder(group.path) : '')
152
- // TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
153
- // A nested folder's module is named by its path (utilities/parsers), so it is not mistaken for its parent's
154
- const moduleName = group.name.includes('/') ? ` ${group.name}` : ''
155
- const moduleComment = description || moduleName ? `/**\n${description ? description.split('\n').map(line => ` * ${line}`.trimEnd()).join('\n') + '\n' : ''} * @module${moduleName}\n */\n\n` : ''
156
- const entryPath = path.join(entryDir, `${group.name}.ts`)
157
- fs.mkdirSync(path.dirname(entryPath), { recursive: true })
158
- fs.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n')
159
- return entryPath
160
- })
161
- }
162
-
163
- const projectName = () => {
164
- try {
165
- return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).name
166
- } catch (error) {
167
- return path.basename(process.cwd())
168
- }
169
- }
170
-
171
- /**
172
- * Generate the HTML documentation of a TypeScript project with TypeDoc, straight from the TypeScript source, so the
173
- * types (and the comments on them) are documented from where they are defined. The modules mirror the folders of the
174
- * source, see {@link writeDocEntries}. Configure this with 'docs.from', 'docs.to', 'docs.index', 'docs.title',
175
- * 'docs.tsconfig' and 'docs.logLevel'.
176
- * @memberOf module:partials
177
- * @param {string} [srcPath=gulpConfig.get('docs.from')] - The directory holding the TypeScript source.
178
- * @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
179
- * @param {function(string): Promise<Object>} [loadDocs] - Resolves the TypeDoc module, tests can inject a stand-in.
180
- * @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
181
- */
182
- export const typeDocsFor = (
183
- srcPath = gulpConfig.get('docs.from'),
184
- outPath = gulpConfig.get('docs.to'),
185
- loadDocs = loadTypeDoc
186
- ) => {
187
- if (gulpConfig.get('docs.enabled') === false) {
188
- return () => {}
189
- }
190
- return (done) => {
191
- const generate = async () => {
192
- const ts = loadTypescript()
193
- const { Application } = await loadDocs()
194
- const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'typedocs-'))
195
- try {
196
- const entryPoints = writeDocEntries(ts, srcPath, path.join(workDir, 'entries'))
197
- // The entry files live outside the project, so the tsconfig which is used for the documentation extends the
198
- // one of the project and adds the entries to the files it includes.
199
- const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config')
200
- const tsconfig = path.join(workDir, 'tsconfig.json')
201
- fs.writeFileSync(tsconfig, JSON.stringify({
202
- ...(projectConfig ? { extends: path.resolve(projectConfig) } : {}),
203
- include: [path.join(workDir, 'entries', '**', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
204
- exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
205
- }))
206
- const indexFile = gulpConfig.get('docs.index')
207
- await removeDirectory(outPath)
208
- const app = await Application.bootstrap({
209
- entryPoints,
210
- tsconfig,
211
- name: gulpConfig.get('docs.title') || projectName(),
212
- readme: indexFile && fs.existsSync(indexFile) ? indexFile : 'none',
213
- logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
214
- skipErrorChecking: true
215
- })
216
- const project = await app.convert()
217
- if (!project) {
218
- throw new Error('TypeDoc could not read the TypeScript source, see the errors above.')
219
- }
220
- await app.generateDocs(project, outPath)
221
- } finally {
222
- await removeDirectory(workDir)
223
- }
224
- }
225
- return typeof done === 'function' ? generate().then(() => done(), done) : generate()
226
- }
227
- }
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 })
24
+ .filter(entry => entry.isFile() && isSource(entry.name))
25
+ .map(entry => path.join(dirPath, entry.name))
26
+
27
+ // Every folder below dirPath (at any depth), named by its path from dirPath, with the source files directly in it
28
+ const listFolders = (dirPath, prefix = '') => fs.readdirSync(dirPath, { withFileTypes: true })
29
+ .filter(entry => entry.isDirectory() && entry.name !== 'node_modules')
30
+ .flatMap(entry => {
31
+ const folderPath = path.join(dirPath, entry.name)
32
+ const name = `${prefix}${entry.name}`
33
+ return [{ name, path: folderPath, files: listSources(folderPath) }, ...listFolders(folderPath, `${name}/`)]
34
+ })
35
+
36
+ const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1')
37
+
38
+ const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(path.sep).join('/')
39
+
40
+ /**
41
+ * Describe what a source file exports: whether it has a default export, and whether it has any named exports.
42
+ * @memberOf module:partials
43
+ * @param {Object} ts - The TypeScript compiler API.
44
+ * @param {string} filePath
45
+ * @returns {{hasDefault: boolean, hasNamed: boolean}}
46
+ */
47
+ export const describeExports = (ts, filePath) => {
48
+ const source = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest)
49
+ let hasDefault = false
50
+ let hasNamed = false
51
+ source.statements.forEach(statement => {
52
+ if (ts.isExportAssignment(statement)) {
53
+ hasDefault = hasDefault || !statement.isExportEquals
54
+ return
55
+ }
56
+ if (ts.isExportDeclaration(statement)) {
57
+ const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null
58
+ if (names && names.some(element => element.name.text === 'default')) {
59
+ hasDefault = true
60
+ }
61
+ if (!names || names.some(element => element.name.text !== 'default')) {
62
+ hasNamed = true
63
+ }
64
+ return
65
+ }
66
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : []
67
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
68
+ if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
69
+ hasDefault = true
70
+ } else {
71
+ hasNamed = true
72
+ }
73
+ }
74
+ })
75
+ return { hasDefault, hasNamed }
76
+ }
77
+
78
+ /**
79
+ * The description of a folder for its module: the first doc comment of the folder's index file, without its tags
80
+ * (@module, @file, ...), or an empty string when there is none.
81
+ * @memberOf module:partials
82
+ * @param {string} folderPath
83
+ * @returns {string}
84
+ */
85
+ export const describeFolder = folderPath => {
86
+ const indexFile = ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(name => path.join(folderPath, name)).find(file => fs.existsSync(file))
87
+ return indexFile ? describeFile(indexFile) : ''
88
+ }
89
+
90
+ /**
91
+ * The description of a source file for its module: its first doc comment without the tags (@module, @file, ...), when
92
+ * that comment is a header (it is followed by an import or export, so it does not document a declaration itself), or
93
+ * an empty string.
94
+ * @memberOf module:partials
95
+ * @param {string} filePath
96
+ * @returns {string}
97
+ */
98
+ export const describeFile = filePath => {
99
+ const comment = fs.readFileSync(filePath, 'utf8').match(/^\s*\/\*\*([\s\S]*?)\*\/\s*(import\b|export\s+(\*|\{[^}]*\}\s+from)|$)/)
100
+ if (!comment) {
101
+ return ''
102
+ }
103
+ const lines = comment[1].split('\n').map(line => line.replace(/^\s*\* ?/, '').trimEnd())
104
+ const tagAt = lines.findIndex(line => /^@\w+/.test(line))
105
+ return (tagAt === -1 ? lines : lines.slice(0, tagAt)).join('\n').trim()
106
+ }
107
+
108
+ /**
109
+ * Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
110
+ * entry file, so the modules of the documentation mirror the folders. Nested folders are modules of their own, named
111
+ * by their path (utilities/parsers), and a folder's module holds only the files directly in it. The default export of each source file is
112
+ * re-exported under the name of the file (the source files hold one function each), and anything else a file exports
113
+ * is re-exported as it is (that is where the types.ts files go). Index files (the barrels), tests and declaration
114
+ * files are left out, but the first doc comment of a folder's index file becomes the description of its module.
115
+ * A file which sits beside the folders (not an index, main or browser file) is a module of its own, described by its header
116
+ * comment. A source directory with no folders gets a single entry called index.
117
+ * @memberOf module:partials
118
+ * @param {Object} ts - The TypeScript compiler API.
119
+ * @param {string} srcDir - The directory holding the TypeScript source.
120
+ * @param {string} entryDir - The directory to write the entry files into.
121
+ * @returns {Array<string>} The paths of the entry files.
122
+ */
123
+ export const writeDocEntries = (ts, srcDir, entryDir) => {
124
+ const absoluteSrc = path.resolve(srcDir)
125
+ fs.mkdirSync(entryDir, { recursive: true })
126
+ const folders = listFolders(absoluteSrc).filter(folder => folder.files.some(file => !/^index\./.test(path.basename(file))))
127
+ const rootFiles = listSources(absoluteSrc).filter(file => !/^(index|main|browser)\./.test(path.basename(file)))
128
+ // Folders make modules, and so do the files which sit beside them (except the barrels: index, main and browser, the
129
+ // browser bundle's entry). A source directory with no folders gets a single module called index which holds its files.
130
+ const groups = folders.length
131
+ ? [
132
+ ...folders,
133
+ ...rootFiles
134
+ .map(file => ({ name: toIdentifier(path.basename(file).replace(/\.[cm]?tsx?$/, '')), files: [file], description: describeFile(file) }))
135
+ .filter(group => !folders.some(folder => folder.name === group.name))
136
+ ]
137
+ : [{ name: 'index', path: absoluteSrc, files: rootFiles }]
138
+ return groups.map(group => {
139
+ const lines = group.files
140
+ .filter(file => !/^index\./.test(path.basename(file)))
141
+ .sort()
142
+ .flatMap(file => {
143
+ const modulePath = toModulePath(file)
144
+ const { hasDefault, hasNamed } = describeExports(ts, file)
145
+ const fileName = path.basename(file).replace(/\.[cm]?tsx?$/, '')
146
+ return [
147
+ ...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []),
148
+ ...(hasNamed ? [`export * from '${modulePath}'`] : [])
149
+ ]
150
+ })
151
+ const description = group.description ?? (group.path ? describeFolder(group.path) : '')
152
+ // TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
153
+ // A nested folder's module is named by its path (utilities/parsers), so it is not mistaken for its parent's
154
+ const moduleName = group.name.includes('/') ? ` ${group.name}` : ''
155
+ const moduleComment = description || moduleName ? `/**\n${description ? description.split('\n').map(line => ` * ${line}`.trimEnd()).join('\n') + '\n' : ''} * @module${moduleName}\n */\n\n` : ''
156
+ const entryPath = path.join(entryDir, `${group.name}.ts`)
157
+ fs.mkdirSync(path.dirname(entryPath), { recursive: true })
158
+ fs.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n')
159
+ return entryPath
160
+ })
161
+ }
162
+
163
+ const projectName = () => {
164
+ try {
165
+ return JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).name
166
+ } catch (error) {
167
+ return path.basename(process.cwd())
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Generate the HTML documentation of a TypeScript project with TypeDoc, straight from the TypeScript source, so the
173
+ * types (and the comments on them) are documented from where they are defined. The modules mirror the folders of the
174
+ * source, see {@link writeDocEntries}. Configure this with 'docs.from', 'docs.to', 'docs.index', 'docs.title',
175
+ * 'docs.tsconfig' and 'docs.logLevel'.
176
+ * @memberOf module:partials
177
+ * @param {string} [srcPath=gulpConfig.get('docs.from')] - The directory holding the TypeScript source.
178
+ * @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
179
+ * @param {function(string): Promise<Object>} [loadDocs] - Resolves the TypeDoc module, tests can inject a stand-in.
180
+ * @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
181
+ */
182
+ export const typeDocsFor = (
183
+ srcPath = gulpConfig.get('docs.from'),
184
+ outPath = gulpConfig.get('docs.to'),
185
+ loadDocs = loadTypeDoc
186
+ ) => {
187
+ if (gulpConfig.get('docs.enabled') === false) {
188
+ return () => {}
189
+ }
190
+ return (done) => {
191
+ const generate = async () => {
192
+ const ts = loadTypescript()
193
+ const { Application } = await loadDocs()
194
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'typedocs-'))
195
+ try {
196
+ const entryPoints = writeDocEntries(ts, srcPath, path.join(workDir, 'entries'))
197
+ // The entry files live outside the project, so the tsconfig which is used for the documentation extends the
198
+ // one of the project and adds the entries to the files it includes.
199
+ const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config')
200
+ const tsconfig = path.join(workDir, 'tsconfig.json')
201
+ fs.writeFileSync(tsconfig, JSON.stringify({
202
+ ...(projectConfig ? { extends: path.resolve(projectConfig) } : {}),
203
+ include: [path.join(workDir, 'entries', '**', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
204
+ exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
205
+ }))
206
+ const indexFile = gulpConfig.get('docs.index')
207
+ const gitRevision = gulpConfig.get('docs.gitRevision')
208
+ await removeDirectory(outPath)
209
+ const app = await Application.bootstrap({
210
+ entryPoints,
211
+ tsconfig,
212
+ name: gulpConfig.get('docs.title') || projectName(),
213
+ readme: indexFile && fs.existsSync(indexFile) ? indexFile : 'none',
214
+ logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
215
+ skipErrorChecking: true,
216
+ // Left out, TypeDoc links each declaration to its source at the current commit, so every commit changes
217
+ // every page; a branch name keeps the pages the same until the documentation itself changes.
218
+ ...(gitRevision ? { gitRevision } : {})
219
+ })
220
+ const project = await app.convert()
221
+ if (!project) {
222
+ throw new Error('TypeDoc could not read the TypeScript source, see the errors above.')
223
+ }
224
+ await app.generateDocs(project, outPath)
225
+ } finally {
226
+ await removeDirectory(workDir)
227
+ }
228
+ }
229
+ return typeof done === 'function' ? generate().then(() => done(), done) : generate()
230
+ }
231
+ }