js-build-tools 6.2.2 → 6.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.
- package/README.md +180 -9
- package/dist/build.js +66 -66
- package/dist/bundle.js +1 -1
- package/dist/docs.js +11 -11
- package/dist/partials/jsdocDocsFor.js +133 -113
- package/dist/partials/loadPlaywright.js +34 -0
- package/dist/partials/neostandardLint.js +5 -3
- package/dist/partials/resolveDocsMethod.js +12 -12
- package/dist/partials/serveStatic.js +57 -0
- package/dist/partials/tsFor.js +2 -2
- package/dist/partials/typeDocsFor.js +28 -22
- package/dist/partials.js +169 -149
- package/dist/partials.min.js +1 -1
- package/dist/test-helpers/setUp.js +6 -0
- package/dist/utilities/convertMjsFilesForDocs.js +1 -0
- package/dist/utilities/dotGet.js +1 -0
- package/dist/utilities/dotNotate.js +5 -0
- package/dist/utilities/dotSet.js +1 -0
- package/dist/utilities/esmNamedExportsToCjs.js +1 -0
- package/dist/utilities/getBundleConfigs.js +1 -0
- package/dist/utilities/isObject.js +1 -0
- package/dist/utilities/strAfter.js +1 -0
- package/dist/utilities/strBefore.js +1 -0
- package/functions/bundle.mjs +1 -1
- package/functions/jsdocEsmExports.test.mjs +24 -0
- package/functions/partials/jsdocDocsFor.mjs +33 -13
- package/functions/partials/jsdocDocsFor.test.mjs +51 -0
- package/functions/partials/loadPlaywright.mjs +23 -0
- package/functions/partials/loadPlaywright.test.mjs +71 -0
- package/functions/partials/neostandardLint.mjs +5 -3
- package/functions/partials/serveStatic.mjs +47 -0
- package/functions/partials/serveStatic.test.mjs +71 -0
- package/functions/partials/tsFor.mjs +2 -2
- package/functions/partials/typeDocsFor.mjs +24 -20
- package/functions/partials/typeDocsFor.test.mjs +23 -0
- package/functions/partials.mjs +2 -0
- package/functions/partials.test.mjs +1 -1
- package/functions/test-helpers/setUp.mjs +6 -0
- package/functions/utilities/convertMjsFilesForDocs.mjs +1 -0
- package/functions/utilities/dotGet.mjs +1 -0
- package/functions/utilities/dotNotate.mjs +5 -0
- package/functions/utilities/dotSet.mjs +1 -0
- package/functions/utilities/esmNamedExportsToCjs.mjs +1 -0
- package/functions/utilities/getBundleConfigs.mjs +1 -0
- package/functions/utilities/isObject.mjs +1 -0
- package/functions/utilities/strAfter.mjs +1 -0
- package/functions/utilities/strBefore.mjs +1 -0
- package/gulp.config.mjs +2 -2
- package/gulpfile.base.mjs +1 -3
- package/jsdoc.base.js +9 -3
- package/jsdoc.esmExports.js +17 -0
- package/package.json +16 -10
|
@@ -2,6 +2,7 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import * as setUp from '../test-helpers/setUp.mjs'
|
|
4
4
|
import { jsdocDocsFor, resolveJsdocBin, resolveJsdocTheme } from './jsdocDocsFor.mjs'
|
|
5
|
+
import jsdocBase from '../../jsdoc.base.js'
|
|
5
6
|
|
|
6
7
|
setUp.setDefaults('test-jsdoc-docs-for')
|
|
7
8
|
const gulpConfig = setUp.gulpConfig
|
|
@@ -57,6 +58,54 @@ describe('jsdocDocsFor', () => {
|
|
|
57
58
|
expect(fs.existsSync(`${outPath}/index.html`)).toBe(true)
|
|
58
59
|
expect(calls.config.opts.destination).toBe(path.resolve(outPath))
|
|
59
60
|
expect(calls.config.source.include).toEqual([path.resolve(gulpConfig.get('srcPath'))])
|
|
61
|
+
// Headings of the front page get ids, so the theme's table of contents can follow them
|
|
62
|
+
expect(calls.config.markdown).toEqual({ idInHeadings: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('passes on what jsdoc reports', async () => {
|
|
66
|
+
gulpConfig.set('docs.enabled', true)
|
|
67
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
68
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
69
|
+
const calls = {}
|
|
70
|
+
const reporting = (bin, args) => fakeRunJsdoc(calls)(bin, args).then(() => ({ stdout: '', stderr: 'WARNING: a page failed\n' }))
|
|
71
|
+
await jsdocDocsFor(srcPath, `${srcPath}/../docs`, reporting)()
|
|
72
|
+
expect(warn).toHaveBeenCalledWith('WARNING: a page failed')
|
|
73
|
+
warn.mockClear()
|
|
74
|
+
await jsdocDocsFor(srcPath, `${srcPath}/../docs`, fakeRunJsdoc(calls))()
|
|
75
|
+
expect(warn).not.toHaveBeenCalled()
|
|
76
|
+
warn.mockRestore()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('takes a list of directories and files to document', async () => {
|
|
80
|
+
gulpConfig.set('docs.enabled', true)
|
|
81
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
82
|
+
const calls = {}
|
|
83
|
+
await jsdocDocsFor([srcPath, `${srcPath}/../gulp.config.mjs`], `${srcPath}/../docs`, fakeRunJsdoc(calls))()
|
|
84
|
+
expect(calls.config.source.include).toEqual([path.resolve(srcPath), path.resolve(`${srcPath}/../gulp.config.mjs`)])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('documents .js, .mjs and .cjs files, but not tests or minified copies', () => {
|
|
88
|
+
const include = new RegExp(jsdocBase.source.includePattern)
|
|
89
|
+
const exclude = new RegExp(jsdocBase.source.excludePattern)
|
|
90
|
+
const documented = file => include.test(file) && !exclude.test(file)
|
|
91
|
+
expect(['a.js', 'a.mjs', 'a.cjs', 'dir/a.mjs'].filter(documented)).toEqual(['a.js', 'a.mjs', 'a.cjs', 'dir/a.mjs'])
|
|
92
|
+
expect(['a.test.mjs', 'a.min.js', 'a.min.mjs', 'dir/_private.mjs', 'a.ts'].filter(documented)).toEqual([])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('passes docs.title to the theme as a link to the home page of the docs and as its page title, and leaves it out when unset', async () => {
|
|
96
|
+
gulpConfig.set('docs.enabled', true)
|
|
97
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
98
|
+
const calls = {}
|
|
99
|
+
gulpConfig.set('docs.title', 'Tom & Jerry <Tools>')
|
|
100
|
+
await jsdocDocsFor(srcPath, `${srcPath}/../docs`, fakeRunJsdoc(calls))()
|
|
101
|
+
expect(calls.config.opts.theme_opts).toEqual({
|
|
102
|
+
title: '<a href="index.html" class="sidebar-title sidebar-title-anchor">Tom & Jerry <Tools></a>',
|
|
103
|
+
homepageTitle: 'Tom & Jerry <Tools>'
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
gulpConfig.set('docs.title', '')
|
|
107
|
+
await jsdocDocsFor(srcPath, `${srcPath}/../docs`, fakeRunJsdoc(calls))()
|
|
108
|
+
expect(calls.config.opts.theme_opts).toBeUndefined()
|
|
60
109
|
})
|
|
61
110
|
|
|
62
111
|
test('uses the configured theme as the template directory', async () => {
|
|
@@ -91,11 +140,13 @@ describe('jsdocDocsFor', () => {
|
|
|
91
140
|
describe('jsdocDocsFor, end to end with the real jsdoc CLI and clean-jsdoc-theme', () => {
|
|
92
141
|
test('produces a real browsable HTML page from real JSDoc comments', async () => {
|
|
93
142
|
gulpConfig.set('docs.enabled', true)
|
|
143
|
+
gulpConfig.set('docs.title', 'Tom & Jerry')
|
|
94
144
|
const outPath = `${gulpConfig.get('srcPath')}/../docs`
|
|
95
145
|
await jsdocDocsFor(gulpConfig.get('srcPath'), outPath)()
|
|
96
146
|
// clean-jsdoc-theme nests its real output under its own route folders (global/, source/...), not flat at
|
|
97
147
|
// outPath's own root - confirmed by actually running it, not assumed from the classic jsdoc template shape.
|
|
98
148
|
const files = fs.readdirSync(outPath, { recursive: true })
|
|
99
149
|
expect(files.some(file => file.endsWith('.html'))).toBe(true)
|
|
150
|
+
expect(fs.readFileSync(`${outPath}/index.html`, 'utf8')).toMatch(/<title>\s*Tom & Jerry\s*<\/title>/)
|
|
100
151
|
}, 30000)
|
|
101
152
|
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { requirePeer } from './loadPeer.mjs'
|
|
2
|
+
|
|
3
|
+
const installHint = 'Install it in your project with: npm install --save-dev playwright'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find Playwright (headless-browser automation) of the project being built, an optional peer dependency only
|
|
7
|
+
* needed by whatever actually drives a real browser - projects which never do never need it installed.
|
|
8
|
+
* @memberOf module:partials
|
|
9
|
+
* @param {string} [projectPath=process.cwd()] The folder of the project (which has the package.json) being built.
|
|
10
|
+
* @returns {Object} The Playwright module.
|
|
11
|
+
* @throws {Error} With install instructions when playwright is not installed.
|
|
12
|
+
*/
|
|
13
|
+
export const loadPlaywright = (projectPath = process.cwd()) => requirePeer('playwright', { task: 'browser automation', installHint, projectPath })
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Launch a real headless Chromium browser via Playwright. See {@link loadPlaywright}.
|
|
17
|
+
* @memberOf module:partials
|
|
18
|
+
* @param {Function} [load=loadPlaywright] Which Playwright to launch - swappable in tests.
|
|
19
|
+
* @param {string} [projectPath=process.cwd()] The folder of the project (which has the package.json) being built.
|
|
20
|
+
* @returns {Promise<Browser>} The running Playwright browser - close it yourself (browser.close()) once done with it.
|
|
21
|
+
* @throws {Error} With install instructions when playwright is not installed.
|
|
22
|
+
*/
|
|
23
|
+
export const launchBrowser = (load = loadPlaywright, projectPath = process.cwd()) => load(projectPath).chromium.launch()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import * as setUp from '../test-helpers/setUp.mjs'
|
|
4
|
+
import { loadPlaywright, launchBrowser } from './loadPlaywright.mjs'
|
|
5
|
+
import { serveStatic } from './serveStatic.mjs'
|
|
6
|
+
|
|
7
|
+
setUp.setDefaults('test-load-playwright')
|
|
8
|
+
const gulpConfig = setUp.gulpConfig
|
|
9
|
+
|
|
10
|
+
beforeEach(setUp.beforeEach)
|
|
11
|
+
afterEach(setUp.afterEach)
|
|
12
|
+
|
|
13
|
+
describe('loadPlaywright', () => {
|
|
14
|
+
test('explains how to install playwright when the project does not have it', () => {
|
|
15
|
+
expect(() => loadPlaywright('/')).toThrow(/optional peer dependency playwright.*npm install --save-dev playwright/)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Unlike typedoc (ESM-only, see typeDocsFor.test.mjs), playwright is CommonJS-compatible, so its real loading can
|
|
19
|
+
// be tested directly here rather than only end to end.
|
|
20
|
+
test('loads the real playwright module from this project (its own devDependency)', () => {
|
|
21
|
+
const playwright = loadPlaywright()
|
|
22
|
+
expect(typeof playwright.chromium.launch).toBe('function')
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('launchBrowser', () => {
|
|
27
|
+
test('resolves playwright (via the given loader) and launches its chromium', async () => {
|
|
28
|
+
const launch = jest.fn(() => Promise.resolve('a-real-browser'))
|
|
29
|
+
const load = jest.fn(() => ({ chromium: { launch } }))
|
|
30
|
+
await expect(launchBrowser(load, '/some/project')).resolves.toBe('a-real-browser')
|
|
31
|
+
expect(load).toHaveBeenCalledWith('/some/project')
|
|
32
|
+
expect(launch).toHaveBeenCalledTimes(1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('defaults to the real loadPlaywright and launches a real, working browser', async () => {
|
|
36
|
+
const browser = await launchBrowser()
|
|
37
|
+
expect(browser.isConnected()).toBe(true)
|
|
38
|
+
await browser.close()
|
|
39
|
+
}, 30000)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('a real page, served and loaded end to end', () => {
|
|
43
|
+
// The exact real-world case that motivated this partial: a page using a root-relative asset path
|
|
44
|
+
// (/style.css), which a bare file:// load resolves against the filesystem root instead of the page's own
|
|
45
|
+
// directory - serveStatic + a real browser is what actually proves this resolves correctly, not jsdom
|
|
46
|
+
// (which cannot execute <script type="module"> at all, the limitation this whole capability exists to close).
|
|
47
|
+
test('a page with a root-relative asset path loads correctly through a real server and a real browser', async () => {
|
|
48
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
49
|
+
fs.mkdirSync(`${srcPath}/browser`, { recursive: true })
|
|
50
|
+
fs.writeFileSync(`${srcPath}/browser/style.css`, 'body { background-color: rgb(1, 2, 3); }')
|
|
51
|
+
fs.writeFileSync(`${srcPath}/index.html`, [
|
|
52
|
+
'<!doctype html><html><head>',
|
|
53
|
+
'<link rel="stylesheet" href="/browser/style.css">',
|
|
54
|
+
'<script type="module">document.title = \'loaded via a real module script\'</script>',
|
|
55
|
+
'</head><body></body></html>'
|
|
56
|
+
].join(''))
|
|
57
|
+
|
|
58
|
+
const { server, url } = await serveStatic(srcPath)
|
|
59
|
+
const browser = await launchBrowser()
|
|
60
|
+
try {
|
|
61
|
+
const page = await browser.newPage()
|
|
62
|
+
await page.goto(`${url}/index.html`)
|
|
63
|
+
expect(await page.title()).toBe('loaded via a real module script')
|
|
64
|
+
const backgroundColor = await page.evaluate(() => getComputedStyle(document.body).backgroundColor)
|
|
65
|
+
expect(backgroundColor).toBe('rgb(1, 2, 3)')
|
|
66
|
+
} finally {
|
|
67
|
+
await browser.close()
|
|
68
|
+
server.close()
|
|
69
|
+
}
|
|
70
|
+
}, 30000)
|
|
71
|
+
})
|
|
@@ -13,6 +13,7 @@ const importNeostandard = () => importModule('neostandard')
|
|
|
13
13
|
* migrating from `standard` keeps its globals (jest, browser, ...) without touching package.json.
|
|
14
14
|
* @param {string} cwd
|
|
15
15
|
* @returns {Array<string>}
|
|
16
|
+
* @private
|
|
16
17
|
*/
|
|
17
18
|
const readEnv = cwd => {
|
|
18
19
|
try {
|
|
@@ -34,7 +35,8 @@ const readEnv = cwd => {
|
|
|
34
35
|
* @param {boolean} options.ts
|
|
35
36
|
* @param {boolean} options.fix
|
|
36
37
|
* @param {Object<string, *>} [options.rules={}]
|
|
37
|
-
* @returns {Promise<
|
|
38
|
+
* @returns {Promise<ESLint>} eslint's ESLint instance
|
|
39
|
+
* @private
|
|
38
40
|
*/
|
|
39
41
|
const buildEslint = async ({ cwd, ts, fix, rules = {} }) => {
|
|
40
42
|
const { neostandard, defaultFilePatterns } = await importNeostandard()
|
|
@@ -53,7 +55,7 @@ const buildEslint = async ({ cwd, ts, fix, rules = {} }) => {
|
|
|
53
55
|
/**
|
|
54
56
|
* Print a file's lint messages, if it has any.
|
|
55
57
|
* @memberOf module:partials
|
|
56
|
-
* @param {
|
|
58
|
+
* @param {Vinyl} file
|
|
57
59
|
* @param {{messages: Array<{line: number, column: number, message: string}>, errorCount: number, warningCount: number}} result
|
|
58
60
|
* @returns {undefined}
|
|
59
61
|
*/
|
|
@@ -82,7 +84,7 @@ const printFileReport = (file, result) => {
|
|
|
82
84
|
* @param {function(Object): Promise<{lintText: Function}>} [loadEslint] - Resolves an ESLint-shaped linter. Real
|
|
83
85
|
* callers should never need to pass this - it exists so tests can supply a stand-in, since this project's
|
|
84
86
|
* CommonJS-transform-based Jest setup cannot execute a real dynamic import() of neostandard (ESM-only).
|
|
85
|
-
* @returns {
|
|
87
|
+
* @returns {stream.Transform}
|
|
86
88
|
*/
|
|
87
89
|
export const neostandardLint = (
|
|
88
90
|
{ fix = true, ts = false, rules = {} } = {},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
// A real, previously-undiscovered bug fixed here: without a Content-Type header, a browser enforces strict MIME
|
|
6
|
+
// checking on <script type="module"> and refuses to load it at all - exactly the case this partial exists for.
|
|
7
|
+
const contentTypes = {
|
|
8
|
+
'.css': 'text/css',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.html': 'text/html',
|
|
11
|
+
'.ico': 'image/x-icon',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.js': 'text/javascript',
|
|
15
|
+
'.json': 'application/json',
|
|
16
|
+
'.mjs': 'text/javascript',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.woff': 'font/woff',
|
|
20
|
+
'.woff2': 'font/woff2'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serve a directory's real files over plain HTTP, for anything that needs a page loaded from a real URL rather
|
|
25
|
+
* than a bare file:// path - most commonly, a page using root-relative asset paths (/browser/...), which a
|
|
26
|
+
* file:// load resolves against the filesystem root instead of the page's own directory.
|
|
27
|
+
* @memberOf module:partials
|
|
28
|
+
* @param {string} rootDir - The directory to serve.
|
|
29
|
+
* @param {number} [port=0] - The port to listen on; 0 picks any free port.
|
|
30
|
+
* @returns {Promise<{server: http.Server, url: string}>} The running server, and its base URL - close the
|
|
31
|
+
* server yourself (server.close()) once done with it.
|
|
32
|
+
*/
|
|
33
|
+
export const serveStatic = (rootDir, port = 0) => new Promise((resolve, reject) => {
|
|
34
|
+
const server = http.createServer((req, res) => {
|
|
35
|
+
const filePath = path.join(rootDir, decodeURIComponent(req.url.split('?')[0]))
|
|
36
|
+
fs.readFile(filePath, (error, data) => {
|
|
37
|
+
if (error) {
|
|
38
|
+
res.statusCode = 404
|
|
39
|
+
return res.end(`Not found: ${req.url}`)
|
|
40
|
+
}
|
|
41
|
+
res.setHeader('Content-Type', contentTypes[path.extname(filePath)] || 'application/octet-stream')
|
|
42
|
+
res.end(data)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
server.once('error', reject)
|
|
46
|
+
server.listen(port, '127.0.0.1', () => resolve({ server, url: `http://127.0.0.1:${server.address().port}` }))
|
|
47
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import * as setUp from '../test-helpers/setUp.mjs'
|
|
4
|
+
import { serveStatic } from './serveStatic.mjs'
|
|
5
|
+
|
|
6
|
+
setUp.setDefaults('test-serve-static')
|
|
7
|
+
const gulpConfig = setUp.gulpConfig
|
|
8
|
+
|
|
9
|
+
const write = (file, contents) => {
|
|
10
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
11
|
+
fs.writeFileSync(file, contents)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(
|
|
15
|
+
() => setUp.beforeEach().then(() => {
|
|
16
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
17
|
+
write(`${srcPath}/index.html`, '<!doctype html><html><body>hello</body></html>')
|
|
18
|
+
write(`${srcPath}/sub/page.html`, '<!doctype html><html><body>nested</body></html>')
|
|
19
|
+
write(`${srcPath}/module.js`, 'export default 1')
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
afterEach(setUp.afterEach)
|
|
24
|
+
|
|
25
|
+
describe('serveStatic', () => {
|
|
26
|
+
test('serves real files from the given directory over a real local HTTP server', async () => {
|
|
27
|
+
const { server, url } = await serveStatic(gulpConfig.get('srcPath'))
|
|
28
|
+
try {
|
|
29
|
+
expect(url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/)
|
|
30
|
+
const root = await fetch(`${url}/index.html`)
|
|
31
|
+
expect(await root.text()).toBe('<!doctype html><html><body>hello</body></html>')
|
|
32
|
+
const nested = await fetch(`${url}/sub/page.html`)
|
|
33
|
+
expect(await nested.text()).toBe('<!doctype html><html><body>nested</body></html>')
|
|
34
|
+
} finally {
|
|
35
|
+
server.close()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('sends a real Content-Type, not a blank one - a browser refuses to load a <script type="module"> without one', async () => {
|
|
40
|
+
const { server, url } = await serveStatic(gulpConfig.get('srcPath'))
|
|
41
|
+
try {
|
|
42
|
+
const html = await fetch(`${url}/index.html`)
|
|
43
|
+
expect(html.headers.get('content-type')).toBe('text/html')
|
|
44
|
+
const script = await fetch(`${url}/module.js`)
|
|
45
|
+
expect(script.headers.get('content-type')).toBe('text/javascript')
|
|
46
|
+
} finally {
|
|
47
|
+
server.close()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('responds 404 for a file that does not exist, rather than hanging or throwing', async () => {
|
|
52
|
+
const { server, url } = await serveStatic(gulpConfig.get('srcPath'))
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${url}/nothing-here.html`)
|
|
55
|
+
expect(response.status).toBe(404)
|
|
56
|
+
} finally {
|
|
57
|
+
server.close()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('two servers for the same directory each get their own free port', async () => {
|
|
62
|
+
const first = await serveStatic(gulpConfig.get('srcPath'))
|
|
63
|
+
const second = await serveStatic(gulpConfig.get('srcPath'))
|
|
64
|
+
try {
|
|
65
|
+
expect(first.url).not.toBe(second.url)
|
|
66
|
+
} finally {
|
|
67
|
+
first.server.close()
|
|
68
|
+
second.server.close()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -9,7 +9,7 @@ const importGulpTsCompile = () => importModule('gulp-ts-compile')
|
|
|
9
9
|
/**
|
|
10
10
|
* Wrap a stream in a promise that resolves once it finishes writing.
|
|
11
11
|
* @memberOf module:partials
|
|
12
|
-
* @param {
|
|
12
|
+
* @param {stream.Stream} stream
|
|
13
13
|
* @returns {Promise<void>}
|
|
14
14
|
*/
|
|
15
15
|
const installHint = 'Install it in your project with: npm install --save-dev typescript'
|
|
@@ -36,7 +36,7 @@ const streamToPromise = (stream) => new Promise((resolve, reject) => {
|
|
|
36
36
|
* gulp-typescript when a removed API call started throwing under TypeScript 7.
|
|
37
37
|
* @memberOf module:partials
|
|
38
38
|
* @param {string} configPath - Path to a tsconfig.json file.
|
|
39
|
-
* @returns {
|
|
39
|
+
* @returns {ts.CompilerOptions} TypeScript's compiler options
|
|
40
40
|
*/
|
|
41
41
|
const readCompilerOptions = (ts, configPath) => {
|
|
42
42
|
const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
@@ -20,13 +20,18 @@ export const loadTypeDoc = (projectPath = process.cwd()) => importPeer('typedoc'
|
|
|
20
20
|
|
|
21
21
|
const isSource = fileName => /\.(ts|tsx|mts|cts)$/.test(fileName) && !/\.d\.[cm]?ts$/.test(fileName) && !/\.(test|spec)\./.test(fileName)
|
|
22
22
|
|
|
23
|
-
const listSources = dirPath => fs.readdirSync(dirPath, { withFileTypes: true })
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
})
|
|
30
35
|
|
|
31
36
|
const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1')
|
|
32
37
|
|
|
@@ -102,11 +107,12 @@ export const describeFile = filePath => {
|
|
|
102
107
|
|
|
103
108
|
/**
|
|
104
109
|
* Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
|
|
105
|
-
* entry file, so the modules of the documentation mirror the folders.
|
|
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
|
|
106
112
|
* re-exported under the name of the file (the source files hold one function each), and anything else a file exports
|
|
107
113
|
* is re-exported as it is (that is where the types.ts files go). Index files (the barrels), tests and declaration
|
|
108
114
|
* files are left out, but the first doc comment of a folder's index file becomes the description of its module.
|
|
109
|
-
* A file which sits beside the folders (not an index or
|
|
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
|
|
110
116
|
* comment. A source directory with no folders gets a single entry called index.
|
|
111
117
|
* @memberOf module:partials
|
|
112
118
|
* @param {Object} ts - The TypeScript compiler API.
|
|
@@ -117,15 +123,10 @@ export const describeFile = filePath => {
|
|
|
117
123
|
export const writeDocEntries = (ts, srcDir, entryDir) => {
|
|
118
124
|
const absoluteSrc = path.resolve(srcDir)
|
|
119
125
|
fs.mkdirSync(entryDir, { recursive: true })
|
|
120
|
-
const folders =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const rootFiles = fs.readdirSync(absoluteSrc, { withFileTypes: true })
|
|
125
|
-
.filter(entry => entry.isFile() && isSource(entry.name) && !/^(index|main)\./.test(entry.name))
|
|
126
|
-
.map(entry => path.join(absoluteSrc, entry.name))
|
|
127
|
-
// Folders make modules, and so do the files which sit beside them (except the barrel: index and main). A source
|
|
128
|
-
// directory with no folders gets a single module called index which holds its files.
|
|
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.
|
|
129
130
|
const groups = folders.length
|
|
130
131
|
? [
|
|
131
132
|
...folders,
|
|
@@ -149,8 +150,11 @@ export const writeDocEntries = (ts, srcDir, entryDir) => {
|
|
|
149
150
|
})
|
|
150
151
|
const description = group.description ?? (group.path ? describeFolder(group.path) : '')
|
|
151
152
|
// TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
|
|
152
|
-
|
|
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` : ''
|
|
153
156
|
const entryPath = path.join(entryDir, `${group.name}.ts`)
|
|
157
|
+
fs.mkdirSync(path.dirname(entryPath), { recursive: true })
|
|
154
158
|
fs.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n')
|
|
155
159
|
return entryPath
|
|
156
160
|
})
|
|
@@ -196,7 +200,7 @@ export const typeDocsFor = (
|
|
|
196
200
|
const tsconfig = path.join(workDir, 'tsconfig.json')
|
|
197
201
|
fs.writeFileSync(tsconfig, JSON.stringify({
|
|
198
202
|
...(projectConfig ? { extends: path.resolve(projectConfig) } : {}),
|
|
199
|
-
include: [path.join(workDir, 'entries', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
|
|
203
|
+
include: [path.join(workDir, 'entries', '**', '*.ts').split(path.sep).join('/'), path.resolve(srcPath).split(path.sep).join('/') + '/**/*.ts'],
|
|
200
204
|
exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
|
|
201
205
|
}))
|
|
202
206
|
const indexFile = gulpConfig.get('docs.index')
|
|
@@ -120,6 +120,7 @@ describe('writeDocEntries', () => {
|
|
|
120
120
|
const srcPath = gulpConfig.get('srcPath')
|
|
121
121
|
write(`${srcPath}/simulate.ts`, "/**\n * Simulate what a user does.\n */\nimport add from './point/add'\nexport const click = () => add(1, 2)\nexport default { click }\n")
|
|
122
122
|
write(`${srcPath}/simulate.test.ts`, "test('x', () => {})\n")
|
|
123
|
+
write(`${srcPath}/browser.ts`, "export * from './main'\n")
|
|
123
124
|
const entryDir = `${srcPath}/../entries`
|
|
124
125
|
const entries = writeDocEntries(ts, srcPath, entryDir)
|
|
125
126
|
expect(entries.map(entry => path.basename(entry)).sort()).toEqual(['line.ts', 'point.ts', 'simulate.ts'])
|
|
@@ -129,6 +130,28 @@ describe('writeDocEntries', () => {
|
|
|
129
130
|
)
|
|
130
131
|
})
|
|
131
132
|
|
|
133
|
+
test('makes a module of each nested folder, named by its path, holding only the files directly in it', () => {
|
|
134
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
135
|
+
write(`${srcPath}/point/polar/to-polar.ts`, 'export default (x: number): number => x\n')
|
|
136
|
+
write(`${srcPath}/point/polar/index.ts`, "/**\n * Polar coordinates.\n */\nexport * from './to-polar'\n")
|
|
137
|
+
write(`${srcPath}/point/deep/index.ts`, "export * from './deeper/far'\n")
|
|
138
|
+
write(`${srcPath}/point/deep/deeper/far.ts`, 'export const far = 1\n')
|
|
139
|
+
const entryDir = `${srcPath}/../entries`
|
|
140
|
+
const entries = writeDocEntries(ts, srcPath, entryDir)
|
|
141
|
+
const absoluteEntries = path.resolve(entryDir)
|
|
142
|
+
expect(entries.map(entry => path.relative(absoluteEntries, entry).split(path.sep).join('/')).sort())
|
|
143
|
+
.toEqual(['line.ts', 'point.ts', 'point/deep/deeper.ts', 'point/polar.ts'])
|
|
144
|
+
const absoluteSrc = path.resolve(srcPath).split(path.sep).join('/')
|
|
145
|
+
expect(fs.readFileSync(`${entryDir}/point.ts`, 'utf8')).not.toMatch(/polar|deep/)
|
|
146
|
+
expect(fs.readFileSync(`${entryDir}/point/polar.ts`, 'utf8')).toBe(
|
|
147
|
+
`/**\n * Polar coordinates.\n * @module point/polar\n */\n\nexport { default as toPolar } from '${absoluteSrc}/point/polar/to-polar'\n`
|
|
148
|
+
)
|
|
149
|
+
// A folder with only an index file (point/deep) has no module, and one without a description is still named
|
|
150
|
+
expect(fs.readFileSync(`${entryDir}/point/deep/deeper.ts`, 'utf8')).toBe(
|
|
151
|
+
`/**\n * @module point/deep/deeper\n */\n\nexport * from '${absoluteSrc}/point/deep/deeper/far'\n`
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
132
155
|
test('makes one entry called index when the source has no folders', () => {
|
|
133
156
|
const flat = `${gulpConfig.get('srcPath')}/../flat`
|
|
134
157
|
write(`${flat}/one.ts`, 'export default 1\n')
|
package/functions/partials.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export { distSeries } from './partials/distSeries.mjs'
|
|
|
17
17
|
export { imagesFor } from './partials/imagesFor.mjs'
|
|
18
18
|
export { importModule, importPeer, requirePeer } from './partials/loadPeer.mjs'
|
|
19
19
|
export { jsdocDocsFor } from './partials/jsdocDocsFor.mjs'
|
|
20
|
+
export { loadPlaywright, launchBrowser } from './partials/loadPlaywright.mjs'
|
|
20
21
|
export { minifyFor } from './partials/minifyFor.mjs'
|
|
21
22
|
export { neostandardLint } from './partials/neostandardLint.mjs'
|
|
22
23
|
export { readmeTemplate } from './partials/readmeTemplate.mjs'
|
|
@@ -24,5 +25,6 @@ export { removeDirectory } from './partials/removeDirectory.mjs'
|
|
|
24
25
|
export { resolveDocsMethod } from './partials/resolveDocsMethod.mjs'
|
|
25
26
|
export { runOnChange } from './partials/runOnChange.mjs'
|
|
26
27
|
export { sassFor } from './partials/sassFor.mjs'
|
|
28
|
+
export { serveStatic } from './partials/serveStatic.mjs'
|
|
27
29
|
export { tsFor } from './partials/tsFor.mjs'
|
|
28
30
|
export { typeDocsFor } from './partials/typeDocsFor.mjs'
|
|
@@ -23,6 +23,12 @@ export const setDefaults = (testDir = 'test-temp') => {
|
|
|
23
23
|
gulpConfig.set('dist.from', `${srcPath}/**/!(*.test).js`)
|
|
24
24
|
gulpConfig.set('dist.main', `${distPath}/main`)
|
|
25
25
|
gulpConfig.set('dist.to', distPath)
|
|
26
|
+
// Reset explicitly, for the same reason as readme.fromEsm below: this project's own build-tools.config.json turns
|
|
27
|
+
// docs on and points them at the website checkout, which would leak into every test that never sets these itself.
|
|
28
|
+
gulpConfig.set('docs.enabled', false)
|
|
29
|
+
gulpConfig.set('docs.from', srcPath)
|
|
30
|
+
gulpConfig.set('docs.to', `${tempDir}docs`)
|
|
31
|
+
gulpConfig.set('docs.title', '')
|
|
26
32
|
gulpConfig.set('fonts.from', `${srcPath}/fonts/**/*`)
|
|
27
33
|
gulpConfig.set('fonts.to', `${browserPath}/fonts`)
|
|
28
34
|
gulpConfig.set('images.from', `${srcPath}/img/**/*.+(png|jpg|jpeg|gif|svg)`)
|
|
@@ -14,6 +14,7 @@ import { esmNamedExportsToCjs } from './esmNamedExportsToCjs.mjs'
|
|
|
14
14
|
* @param {Array<string>} mjsFilePaths - Real `.mjs` files to convert. An empty array is a safe, cheap no-op.
|
|
15
15
|
* @returns {{filePaths: Array<string>, cleanUp: function(): void}} The temp `.js` file paths to pass to
|
|
16
16
|
* jsdoc-to-markdown instead of the real `.mjs` ones, and a function to remove the temp directory afterward.
|
|
17
|
+
* @private
|
|
17
18
|
*/
|
|
18
19
|
export const convertMjsFilesForDocs = (mjsFilePaths = []) => {
|
|
19
20
|
if (mjsFilePaths.length === 0) {
|
|
@@ -9,6 +9,7 @@ import { strBefore } from './strBefore.mjs'
|
|
|
9
9
|
* @param {string} [dotNotation=''] - The path to the property
|
|
10
10
|
* @param {string|null} [defaultValue=null] - The default value to return if the property is not found
|
|
11
11
|
* @returns {*} The value of the property
|
|
12
|
+
* @private
|
|
12
13
|
*/
|
|
13
14
|
export const dotGet = (arrayObject = {}, dotNotation= '', defaultValue = null) => {
|
|
14
15
|
if (!dotNotation) {
|
|
@@ -4,12 +4,14 @@ import { isObject } from './isObject.mjs'
|
|
|
4
4
|
* Convert an array of keys into a regex, return a function to test if incoming keys match.
|
|
5
5
|
* @param {Array.<string>} [retainObjects=[]] - An array of keys to retain as objects
|
|
6
6
|
* @returns {Function} The dot-notated array
|
|
7
|
+
* @private
|
|
7
8
|
*/
|
|
8
9
|
const handleRetainObjects = (retainObjects = []) => {
|
|
9
10
|
if (!retainObjects.length) {
|
|
10
11
|
/**
|
|
11
12
|
* Bypass the test function if there are no retainObjects.
|
|
12
13
|
* @returns {false}
|
|
14
|
+
* @private
|
|
13
15
|
*/
|
|
14
16
|
return (currentKey, value, results) => false
|
|
15
17
|
}
|
|
@@ -21,6 +23,7 @@ const handleRetainObjects = (retainObjects = []) => {
|
|
|
21
23
|
* @param {*} value - The value of the key
|
|
22
24
|
* @param {Object} results - The result object to add to
|
|
23
25
|
* @returns {boolean}
|
|
26
|
+
* @private
|
|
24
27
|
*/
|
|
25
28
|
return (currentKey, value, results) => {
|
|
26
29
|
if (!currentKey.match(retainRegex)) {
|
|
@@ -38,6 +41,7 @@ const handleRetainObjects = (retainObjects = []) => {
|
|
|
38
41
|
* @param {string} [prepend=''] - The path for the property being processed
|
|
39
42
|
* @param {Object<string, *>} [results={}] - The final notated object to return
|
|
40
43
|
* @returns {Object<string, *>} The dot-notated object
|
|
44
|
+
* @private
|
|
41
45
|
*/
|
|
42
46
|
const performDotNotate = (arrayObject, didRetain, prepend = '', results = {}) => {
|
|
43
47
|
for (let key in arrayObject) {
|
|
@@ -64,5 +68,6 @@ const performDotNotate = (arrayObject, didRetain, prepend = '', results = {}) =>
|
|
|
64
68
|
* @param {Object} arrayObject - The array or object to dot-notate
|
|
65
69
|
* @param {Array.<string>} [retainObjects=[]] - An array of keys to retain as objects
|
|
66
70
|
* @returns {Object<string, *>} The dot-notated object
|
|
71
|
+
* @private
|
|
67
72
|
*/
|
|
68
73
|
export const dotNotate = (arrayObject, retainObjects = []) => performDotNotate(arrayObject, handleRetainObjects(retainObjects))
|
|
@@ -9,6 +9,7 @@ import { strBefore } from './strBefore.mjs'
|
|
|
9
9
|
* @param {string} dotNotation - The path for the property
|
|
10
10
|
* @param {*} value - The default value to return if the property is not found
|
|
11
11
|
* @returns {Object} The modified object
|
|
12
|
+
* @private
|
|
12
13
|
*/
|
|
13
14
|
export const dotSet = (arrayObject, dotNotation, value = null) => {
|
|
14
15
|
let key = strBefore(dotNotation, '.')
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @memberOf module:utilities
|
|
13
13
|
* @param {string} source - The raw ESM source text (never executed, just rewritten as text).
|
|
14
14
|
* @returns {string} The rewritten text, safe to write out with a `.js` extension for jsdoc-to-markdown to read.
|
|
15
|
+
* @private
|
|
15
16
|
*/
|
|
16
17
|
export const esmNamedExportsToCjs = (source) => {
|
|
17
18
|
const exportedNames = []
|
|
@@ -8,6 +8,7 @@ import * as gulpConfig from '../../gulp.config.mjs'
|
|
|
8
8
|
* specify what's actually different about it.
|
|
9
9
|
* @memberOf module:utilities
|
|
10
10
|
* @returns {Array<{name: string, main: string, ignore: Array, exclude: Array}>}
|
|
11
|
+
* @private
|
|
11
12
|
*/
|
|
12
13
|
export const getBundleConfigs = () => {
|
|
13
14
|
const primary = {
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
* Original source concepts from {@link https://github.com/jheagle/si-funciona/blob/main/src/helpers/objects/isObject.ts Sí, funciona}
|
|
4
4
|
* @param {*} object
|
|
5
5
|
* @returns {boolean}
|
|
6
|
+
* @private
|
|
6
7
|
*/
|
|
7
8
|
export const isObject = (object) => typeof object === 'object' && object !== null
|
package/gulp.config.mjs
CHANGED
|
@@ -133,8 +133,8 @@ import { fileExists } from 'test-filesystem'
|
|
|
133
133
|
* @typedef {Object<string, Setting>} module:gulpConfig~DocsConfig
|
|
134
134
|
* @property {BooleanSetting} enabled - Toggle generating the documentation from the source. When it is on the
|
|
135
135
|
* build does this instead of generating the readme from the jsdoc comments of the compiled files.
|
|
136
|
-
* @property {
|
|
137
|
-
* is scanned recursively (jsdoc).
|
|
136
|
+
* @property {ArrayableSetting} from - The directory holding the source, each folder in it becomes a module (TypeDoc) or
|
|
137
|
+
* is scanned recursively (jsdoc). jsdoc also takes a list of directories and files.
|
|
138
138
|
* @property {StringSetting} index - The markdown file used as the front page of the documentation.
|
|
139
139
|
* @property {StringSetting} logLevel - How much TypeDoc reports: Verbose, Info, Warn, Error or None. Not used by jsdoc.
|
|
140
140
|
* @property {FlagStringSetting} method - 'typedoc' or 'jsdoc'. False (the default) infers it from typescript.enabled -
|
package/gulpfile.base.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @file
|
|
2
|
+
* Import these functions to your own project to customize your build pipeline.
|
|
4
3
|
* @author Joshua Heagle <joshuaheagle@gmail.com>
|
|
5
|
-
* @version 3.0.0
|
|
6
4
|
* @module js-build-tools
|
|
7
5
|
*/
|
|
8
6
|
export { build } from './functions/build.mjs'
|