js-build-tools 5.7.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/dist/build.js +40 -38
- package/dist/build.min.js +1 -1
- package/dist/bundle.js +24 -23
- package/dist/bundle.min.js +1 -1
- package/dist/bundleMinify.js +14 -13
- package/dist/bundleMinify.min.js +1 -1
- package/dist/compileReadme.js +7 -7
- package/dist/copyFonts.js +14 -13
- package/dist/copyFonts.min.js +1 -1
- package/dist/default.js +16 -15
- package/dist/default.min.js +1 -1
- package/dist/dist.js +7 -7
- package/dist/distMinify.js +14 -13
- package/dist/distMinify.min.js +1 -1
- package/dist/images.js +7 -7
- package/dist/partials/addToReadme.js +3 -2
- package/dist/partials/beginWatcher.js +13 -12
- package/dist/partials/clean.js +15 -14
- package/dist/partials/distFor.js +22 -21
- package/dist/partials/distForSrc.js +12 -11
- package/dist/partials/distSeries.js +20 -19
- package/dist/partials/imagesFor.js +2 -1
- package/dist/partials/loadPeer.js +70 -70
- package/dist/partials/neostandardLint.js +163 -0
- package/dist/partials/readmeTemplate.js +15 -14
- package/dist/partials/runOnChange.js +29 -28
- package/dist/partials/sassFor.js +29 -28
- package/dist/partials/tsFor.js +2 -1
- package/dist/partials/typeDocsFor.js +2 -1
- package/dist/partials.js +136 -136
- package/dist/partials.min.js +1 -1
- package/dist/sass.js +7 -7
- package/dist/sourceLint.js +35 -0
- package/dist/sourceLint.min.js +1 -0
- package/dist/test-helpers/neostandardLintWithRealNeostandard.js +50 -0
- package/dist/test-helpers/setUp.js +2 -1
- package/dist/testFull.js +21 -20
- package/dist/testFull.min.js +1 -1
- package/dist/testHelpers.js +11 -10
- package/dist/testHelpers.min.js +1 -1
- package/dist/testQuick.js +23 -22
- package/dist/testQuick.min.js +1 -1
- package/dist/typeDocs.js +9 -9
- package/dist/typeScript.js +6 -6
- package/dist/utilities/convertMjsFilesForDocs.js +36 -36
- package/dist/utilities/dotNotate.js +48 -46
- package/dist/utilities/esmNamedExportsToCjs.js +18 -18
- package/dist/utilities/getBundleConfigs.js +3 -2
- package/dist/watchFull.js +7 -7
- package/dist/watchTest.js +17 -16
- package/dist/watchTest.min.js +1 -1
- package/functions/build.mjs +9 -8
- package/functions/build.test.mjs +18 -25
- package/functions/bundle.mjs +2 -2
- package/functions/partials/addToReadme.mjs +1 -1
- package/functions/partials/neostandardLint.mjs +130 -0
- package/functions/partials/neostandardLint.test.mjs +117 -0
- package/functions/partials.mjs +1 -1
- package/functions/sourceLint.mjs +24 -0
- package/functions/sourceLint.test.mjs +97 -0
- package/functions/test-helpers/neostandardLintWithRealNeostandard.mjs +40 -0
- package/functions/utilities/dotNotate.mjs +3 -1
- package/functions/utilities/dotNotate.test.mjs +34 -0
- package/functions/utilities/getBundleConfigs.mjs +1 -1
- package/gulp.config.mjs +14 -0
- package/gulpfile.base.js +2 -4
- package/gulpfile.base.mjs +1 -2
- package/package.json +7 -6
- package/dist/bundleLint.js +0 -28
- package/dist/bundleLint.min.js +0 -1
- package/dist/distLint.js +0 -20
- package/dist/distLint.min.js +0 -1
- package/dist/partials/standardLint.js +0 -94
- package/dist/test-helpers/standardLintWithRealStandard.js +0 -28
- package/functions/bundleLint.mjs +0 -18
- package/functions/bundleLint.test.mjs +0 -85
- package/functions/distLint.mjs +0 -12
- package/functions/distLint.test.mjs +0 -62
- package/functions/partials/standardLint.mjs +0 -78
- package/functions/partials/standardLint.test.mjs +0 -99
- package/functions/test-helpers/standardLintWithRealStandard.mjs +0 -21
package/functions/build.test.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import { testFull } from './testFull.mjs'
|
|
|
6
6
|
import { typeDocs } from './typeDocs.mjs'
|
|
7
7
|
import { countMatches } from 'test-filesystem'
|
|
8
8
|
|
|
9
|
-
// Run the real
|
|
10
|
-
jest.mock('./partials/
|
|
9
|
+
// Run the real neostandard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
|
|
10
|
+
jest.mock('./partials/neostandardLint.mjs', () => require('./test-helpers/neostandardLintWithRealNeostandard.mjs'))
|
|
11
11
|
|
|
12
12
|
setUp.setDefaults('test-build')
|
|
13
13
|
const gulpConfig = setUp.gulpConfig
|
|
@@ -266,7 +266,7 @@ beforeEach(
|
|
|
266
266
|
afterEach(setUp.afterEach)
|
|
267
267
|
|
|
268
268
|
describe('build', () => {
|
|
269
|
-
test('when browser.enabled false: calls clean,
|
|
269
|
+
test('when browser.enabled false: calls clean, sourceLint, distSeries, distMinify, compileReadme, testFull', done => {
|
|
270
270
|
const srcPath = gulpConfig.get('srcPath')
|
|
271
271
|
const sourcesPath = `${srcPath}/sources`
|
|
272
272
|
const distPath = gulpConfig.get('dist.to')
|
|
@@ -288,16 +288,15 @@ describe('build', () => {
|
|
|
288
288
|
// Minify the dist files (distMinify)
|
|
289
289
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
290
290
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
291
|
-
|
|
292
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
291
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
293
292
|
|
|
294
293
|
expect(fs.existsSync(`${distSourcesPath}/file1.min.js`)).toBeTruthy()
|
|
295
294
|
const babelifiedFile1 = fs.readFileSync(`${distSourcesPath}/file1.js`).toString()
|
|
296
|
-
expect(
|
|
295
|
+
expect(babelifiedFile1.includes('use strict')).toBe(true)
|
|
297
296
|
|
|
298
297
|
expect(fs.existsSync(`${distSourcesPath}/file2.min.js`)).toBeTruthy()
|
|
299
298
|
const babelifiedFile2 = fs.readFileSync(`${distSourcesPath}/file2.js`).toString()
|
|
300
|
-
expect(
|
|
299
|
+
expect(babelifiedFile2.includes('use strict')).toBe(true)
|
|
301
300
|
|
|
302
301
|
// Generated README.md (compileReadme)
|
|
303
302
|
const readmeContents = fs.readFileSync(`${readmePath}/${readmeFile}`).toString()
|
|
@@ -312,7 +311,7 @@ describe('build', () => {
|
|
|
312
311
|
})
|
|
313
312
|
}, 30000)
|
|
314
313
|
|
|
315
|
-
test('when browser.enabled true: calls clean,
|
|
314
|
+
test('when browser.enabled true: calls clean, sourceLint, distSeries, distMinify, bundle, bundleMinify, compileReadme, testFull', done => {
|
|
316
315
|
const srcPath = gulpConfig.get('srcPath')
|
|
317
316
|
const sourcesPath = `${srcPath}/sources`
|
|
318
317
|
const distPath = gulpConfig.get('dist.to')
|
|
@@ -336,8 +335,7 @@ describe('build', () => {
|
|
|
336
335
|
// Minify the dist files (distMinify)
|
|
337
336
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
338
337
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
339
|
-
|
|
340
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
338
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
341
339
|
|
|
342
340
|
// Created browser directory (bundle)
|
|
343
341
|
expect(fs.existsSync(browserPath)).toBeTruthy()
|
|
@@ -361,7 +359,7 @@ describe('build', () => {
|
|
|
361
359
|
})
|
|
362
360
|
}, 30000)
|
|
363
361
|
|
|
364
|
-
test('when browser.enabled true and fonts.enabled true: calls clean,
|
|
362
|
+
test('when browser.enabled true and fonts.enabled true: calls clean, sourceLint, distSeries, distMinify, bundle, bundleMinify, compileReadme, testFull, copyFonts', done => {
|
|
365
363
|
const srcPath = gulpConfig.get('srcPath')
|
|
366
364
|
const sourcesPath = `${srcPath}/sources`
|
|
367
365
|
const distPath = gulpConfig.get('dist.to')
|
|
@@ -388,8 +386,7 @@ describe('build', () => {
|
|
|
388
386
|
// Minify the dist files (distMinify)
|
|
389
387
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
390
388
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
391
|
-
|
|
392
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
389
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
393
390
|
|
|
394
391
|
// Created browser directory (bundle)
|
|
395
392
|
expect(fs.existsSync(browserPath)).toBeTruthy()
|
|
@@ -419,7 +416,7 @@ describe('build', () => {
|
|
|
419
416
|
})
|
|
420
417
|
}, 30000)
|
|
421
418
|
|
|
422
|
-
test('when browser.enabled true and images.enabled true: calls clean,
|
|
419
|
+
test('when browser.enabled true and images.enabled true: calls clean, sourceLint, distSeries, distMinify, bundle, bundleMinify, compileReadme, testFull, images', done => {
|
|
423
420
|
const srcPath = gulpConfig.get('srcPath')
|
|
424
421
|
const sourcesPath = `${srcPath}/sources`
|
|
425
422
|
const distPath = gulpConfig.get('dist.to')
|
|
@@ -446,8 +443,7 @@ describe('build', () => {
|
|
|
446
443
|
// Minify the dist files (distMinify)
|
|
447
444
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
448
445
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
449
|
-
|
|
450
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
446
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
451
447
|
|
|
452
448
|
// Created browser directory (bundle)
|
|
453
449
|
expect(fs.existsSync(browserPath)).toBeTruthy()
|
|
@@ -477,7 +473,7 @@ describe('build', () => {
|
|
|
477
473
|
})
|
|
478
474
|
}, 30000)
|
|
479
475
|
|
|
480
|
-
test('when browser.enabled true and sass.enabled true: calls clean,
|
|
476
|
+
test('when browser.enabled true and sass.enabled true: calls clean, sourceLint, distSeries, distMinify, bundle, bundleMinify, compileReadme, testFull, sass', done => {
|
|
481
477
|
const srcPath = gulpConfig.get('srcPath')
|
|
482
478
|
const sourcesPath = `${srcPath}/sources`
|
|
483
479
|
const distPath = gulpConfig.get('dist.to')
|
|
@@ -504,8 +500,7 @@ describe('build', () => {
|
|
|
504
500
|
// Minify the dist files (distMinify)
|
|
505
501
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
506
502
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
507
|
-
|
|
508
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
503
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
509
504
|
|
|
510
505
|
// Created browser directory (bundle)
|
|
511
506
|
expect(fs.existsSync(browserPath)).toBeTruthy()
|
|
@@ -537,7 +532,7 @@ describe('build', () => {
|
|
|
537
532
|
})
|
|
538
533
|
}, 30000)
|
|
539
534
|
|
|
540
|
-
test('when browser.enabled false and typescript.enabled set: calls clean, tsFor, distFor,
|
|
535
|
+
test('when browser.enabled false and typescript.enabled set: calls clean, sourceLint, tsFor, distFor, distMinify, compileReadme, testFull', done => {
|
|
541
536
|
const srcPath = gulpConfig.get('srcPath')
|
|
542
537
|
const rootPath = gulpConfig.get('rootPath')
|
|
543
538
|
const tsConfig = `${rootPath}/tsconfig.json`
|
|
@@ -563,8 +558,7 @@ describe('build', () => {
|
|
|
563
558
|
// Minify the dist files (distMinify)
|
|
564
559
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
565
560
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
566
|
-
|
|
567
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
561
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
568
562
|
|
|
569
563
|
// Generated README.md (compileReadme)
|
|
570
564
|
const readmeContents = fs.readFileSync(`${readmePath}/${readmeFile}`).toString()
|
|
@@ -577,7 +571,7 @@ describe('build', () => {
|
|
|
577
571
|
})
|
|
578
572
|
}, 30000)
|
|
579
573
|
|
|
580
|
-
test('when browser.enabled true and typescript.enabled set: calls clean, tsFor, distFor,
|
|
574
|
+
test('when browser.enabled true and typescript.enabled set: calls clean, sourceLint, tsFor, distFor, distMinify, bundle, bundleMinify, compileReadme, testFull', done => {
|
|
581
575
|
const srcPath = gulpConfig.get('srcPath')
|
|
582
576
|
const rootPath = gulpConfig.get('rootPath')
|
|
583
577
|
const tsConfig = `${rootPath}/tsconfig.json`
|
|
@@ -606,8 +600,7 @@ describe('build', () => {
|
|
|
606
600
|
// Minify the dist files (distMinify)
|
|
607
601
|
expect(fs.existsSync(`${distPath}/main.min.js`)).toBeTruthy()
|
|
608
602
|
const babelifiedMain = fs.readFileSync(`${distPath}/main.js`).toString()
|
|
609
|
-
|
|
610
|
-
expect(countMatches(babelifiedMain, '\'use strict\'')).toEqual(1)
|
|
603
|
+
expect(babelifiedMain.includes('use strict')).toBe(true)
|
|
611
604
|
|
|
612
605
|
// Created browser directory (bundle)
|
|
613
606
|
expect(fs.existsSync(browserPath)).toBeTruthy()
|
package/functions/bundle.mjs
CHANGED
|
@@ -28,10 +28,10 @@ import { getBundleConfigs } from './utilities/getBundleConfigs.mjs'
|
|
|
28
28
|
export const bundle = () => mergeStream(
|
|
29
29
|
getBundleConfigs().map(({ name, main, ignore, exclude }) => {
|
|
30
30
|
const bundler = browserify(main, { standalone: name })
|
|
31
|
-
if (ignore
|
|
31
|
+
if (ignore.length) {
|
|
32
32
|
bundler.ignore(ignore)
|
|
33
33
|
}
|
|
34
|
-
if (exclude
|
|
34
|
+
if (exclude.length) {
|
|
35
35
|
bundler.exclude(exclude)
|
|
36
36
|
}
|
|
37
37
|
return bundler
|
|
@@ -15,7 +15,7 @@ import { convertMjsFilesForDocs } from '../utilities/convertMjsFilesForDocs.mjs'
|
|
|
15
15
|
* @returns {string|Uint8Array}
|
|
16
16
|
*/
|
|
17
17
|
export const addToReadme = () => {
|
|
18
|
-
const { filePaths: esmFilePaths, cleanUp } = convertMjsFilesForDocs(globSync(gulpConfig.get('readme.fromEsm')
|
|
18
|
+
const { filePaths: esmFilePaths, cleanUp } = convertMjsFilesForDocs(globSync(gulpConfig.get('readme.fromEsm')))
|
|
19
19
|
return jsdoc2md
|
|
20
20
|
.render({ files: [...globSync(gulpConfig.get('readme.from')), ...esmFilePaths] })
|
|
21
21
|
.finally(cleanUp)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import through from 'through2'
|
|
4
|
+
import { ESLint } from 'eslint'
|
|
5
|
+
import { importModule } from './loadPeer.mjs'
|
|
6
|
+
|
|
7
|
+
// neostandard (js-build-tools' chosen style - a maintained, TypeScript-aware successor to `standard`, see
|
|
8
|
+
// https://github.com/neostandard/neostandard) ships ESM-only, see importModule for how it is imported.
|
|
9
|
+
const importNeostandard = () => importModule('neostandard')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read the `env` a project already declared for Standard (the `standard` field in its package.json), so a project
|
|
13
|
+
* migrating from `standard` keeps its globals (jest, browser, ...) without touching package.json.
|
|
14
|
+
* @param {string} cwd
|
|
15
|
+
* @returns {Array<string>}
|
|
16
|
+
*/
|
|
17
|
+
const readEnv = cwd => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).standard?.env ?? []
|
|
20
|
+
} catch {
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build an ESLint instance configured with neostandard, honoring the project's own package.json `standard.env`,
|
|
27
|
+
* plus an optional layer of rule overrides (see gulp.config.mjs's `lint.rules`) for a rule that is wrong for this
|
|
28
|
+
* project specifically rather than something to fix in its code - for example a test fixture whose content
|
|
29
|
+
* neostandard mistakes for a real syntax problem. Scoped to the same files neostandard itself lints (its own
|
|
30
|
+
* `defaultFilePatterns.all`), per neostandard's own guidance: an unscoped rule-tweak layer would otherwise apply
|
|
31
|
+
* to every lintable file, including ones from an unrelated plugin this project never defined.
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {string} options.cwd
|
|
34
|
+
* @param {boolean} options.ts
|
|
35
|
+
* @param {boolean} options.fix
|
|
36
|
+
* @param {Object<string, *>} [options.rules={}]
|
|
37
|
+
* @returns {Promise<import('eslint').ESLint>}
|
|
38
|
+
*/
|
|
39
|
+
const buildEslint = async ({ cwd, ts, fix, rules = {} }) => {
|
|
40
|
+
const { neostandard, defaultFilePatterns } = await importNeostandard()
|
|
41
|
+
const baseConfig = neostandard({ ts, env: readEnv(cwd) })
|
|
42
|
+
if (Object.keys(rules).length) {
|
|
43
|
+
baseConfig.push({ files: defaultFilePatterns.all, rules })
|
|
44
|
+
}
|
|
45
|
+
return new ESLint({
|
|
46
|
+
cwd,
|
|
47
|
+
fix,
|
|
48
|
+
overrideConfigFile: true,
|
|
49
|
+
baseConfig
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print a file's lint messages, if it has any.
|
|
55
|
+
* @memberOf module:partials
|
|
56
|
+
* @param {import('vinyl')} file
|
|
57
|
+
* @param {{messages: Array<{line: number, column: number, message: string}>, errorCount: number, warningCount: number}} result
|
|
58
|
+
* @returns {undefined}
|
|
59
|
+
*/
|
|
60
|
+
const printFileReport = (file, result) => {
|
|
61
|
+
if (!result.messages.length) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
console.log(path.relative(file.cwd, file.path))
|
|
65
|
+
result.messages.forEach(({ line, column, message }) => console.log(`line ${line}:${column}\t${message}`))
|
|
66
|
+
console.log(`✖ ${result.errorCount} error\t⚠ ${result.warningCount} warning\n`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lint every file streaming through with neostandard and, unless `fix` is false, apply the automatic fixes to the
|
|
71
|
+
* file's contents so a following `dest()` writes the fixed version. Remaining problems are printed along with a
|
|
72
|
+
* summary once the stream ends; they never fail the build.
|
|
73
|
+
*
|
|
74
|
+
* One ESLint instance is built from the first file's own cwd and reused for the rest of the stream, since every
|
|
75
|
+
* file in a given lint pass shares the same project (and so the same config).
|
|
76
|
+
* @memberOf module:partials
|
|
77
|
+
* @param {Object} [options={}]
|
|
78
|
+
* @param {boolean} [options.fix=true] - Apply neostandard's automatic fixes to the file contents.
|
|
79
|
+
* @param {boolean} [options.ts=false] - Lint `.ts` files too, with type-aware rules (see neostandard's `ts` option).
|
|
80
|
+
* @param {Object<string, *>} [options.rules={}] - ESLint rule overrides layered on top of neostandard's own rules
|
|
81
|
+
* (see gulp.config.mjs's `lint.rules`).
|
|
82
|
+
* @param {function(Object): Promise<{lintText: Function}>} [loadEslint] - Resolves an ESLint-shaped linter. Real
|
|
83
|
+
* callers should never need to pass this - it exists so tests can supply a stand-in, since this project's
|
|
84
|
+
* CommonJS-transform-based Jest setup cannot execute a real dynamic import() of neostandard (ESM-only).
|
|
85
|
+
* @returns {import('stream').Transform}
|
|
86
|
+
*/
|
|
87
|
+
export const neostandardLint = (
|
|
88
|
+
{ fix = true, ts = false, rules = {} } = {},
|
|
89
|
+
loadEslint = buildEslint
|
|
90
|
+
) => {
|
|
91
|
+
const totals = { errors: 0, warnings: 0 }
|
|
92
|
+
let eslintPromise = null
|
|
93
|
+
return through.obj(
|
|
94
|
+
function (file, enc, callback) {
|
|
95
|
+
if (file.isNull()) {
|
|
96
|
+
callback(null, file)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (file.isStream()) {
|
|
100
|
+
callback(new Error('neostandardLint: streaming file contents are not supported.'))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
if (!eslintPromise) {
|
|
104
|
+
eslintPromise = loadEslint({ cwd: file.cwd, ts, fix, rules })
|
|
105
|
+
}
|
|
106
|
+
eslintPromise
|
|
107
|
+
.then(eslint => eslint.lintText(file.contents.toString(), { filePath: file.path }))
|
|
108
|
+
.then(results => {
|
|
109
|
+
// No result at all means the file is ignored by the project's ESLint configuration.
|
|
110
|
+
const [result] = results
|
|
111
|
+
if (result) {
|
|
112
|
+
if (typeof result.output === 'string') {
|
|
113
|
+
file.contents = Buffer.from(result.output)
|
|
114
|
+
}
|
|
115
|
+
totals.errors += result.errorCount
|
|
116
|
+
totals.warnings += result.warningCount
|
|
117
|
+
printFileReport(file, result)
|
|
118
|
+
}
|
|
119
|
+
callback(null, file)
|
|
120
|
+
}, callback)
|
|
121
|
+
},
|
|
122
|
+
function (callback) {
|
|
123
|
+
console.log('neostandard linter results')
|
|
124
|
+
console.log('======================================')
|
|
125
|
+
console.log(`✖ Errors total: ${totals.errors}`)
|
|
126
|
+
console.log(`⚠ Warnings total: ${totals.warnings}\n`)
|
|
127
|
+
callback()
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Vinyl from 'vinyl'
|
|
2
|
+
import { Readable } from 'node:stream'
|
|
3
|
+
import { neostandardLint } from './neostandardLint.mjs'
|
|
4
|
+
|
|
5
|
+
const makeFile = (contents, filePath = '/project/src/example.js') => new Vinyl({
|
|
6
|
+
cwd: '/project',
|
|
7
|
+
base: '/project/src',
|
|
8
|
+
path: filePath,
|
|
9
|
+
contents
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const run = (transform, files) => new Promise((resolve, reject) => {
|
|
13
|
+
const out = []
|
|
14
|
+
transform.on('data', file => out.push(file)).on('end', () => resolve(out)).on('error', reject)
|
|
15
|
+
files.forEach(file => transform.write(file))
|
|
16
|
+
transform.end()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const fakeEslint = (resultFor) => ({
|
|
20
|
+
lintText: jest.fn((text, options) => Promise.resolve(resultFor(text, options)))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
let logSpy
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
logSpy.mockRestore()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('neostandardLint', () => {
|
|
34
|
+
test('replaces file contents with the fixed output and hands ESLint the file path', async () => {
|
|
35
|
+
expect.assertions(3)
|
|
36
|
+
const eslint = fakeEslint((text) => [{ messages: [], errorCount: 0, warningCount: 0, output: text.replace(/"/g, '\'') }])
|
|
37
|
+
const [linted] = await run(neostandardLint({ fix: true }, () => Promise.resolve(eslint)), [makeFile(Buffer.from('const a = "b"'))])
|
|
38
|
+
expect(linted.contents.toString()).toEqual('const a = \'b\'')
|
|
39
|
+
expect(eslint.lintText).toHaveBeenCalledWith('const a = "b"', { filePath: '/project/src/example.js' })
|
|
40
|
+
expect(linted.path).toEqual('/project/src/example.js')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('leaves the contents alone when ESLint has no fixed output (nothing needed fixing)', async () => {
|
|
44
|
+
expect.assertions(1)
|
|
45
|
+
const eslint = fakeEslint(() => [{ messages: [], errorCount: 0, warningCount: 0 }])
|
|
46
|
+
const [linted] = await run(neostandardLint({}, () => Promise.resolve(eslint)), [makeFile(Buffer.from('const a = 1'))])
|
|
47
|
+
expect(linted.contents.toString()).toEqual('const a = 1')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('builds the linter once per stream, from the first file, and reuses it for the rest', async () => {
|
|
51
|
+
expect.assertions(1)
|
|
52
|
+
const loadEslint = jest.fn(() => Promise.resolve(fakeEslint(() => [{ messages: [], errorCount: 0, warningCount: 0 }])))
|
|
53
|
+
await run(
|
|
54
|
+
neostandardLint({}, loadEslint),
|
|
55
|
+
[makeFile(Buffer.from('a'), '/project/src/a.js'), makeFile(Buffer.from('b'), '/project/src/b.js')]
|
|
56
|
+
)
|
|
57
|
+
expect(loadEslint).toHaveBeenCalledTimes(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('passes fix and ts through to the loader', async () => {
|
|
61
|
+
expect.assertions(1)
|
|
62
|
+
const loadEslint = jest.fn(() => Promise.resolve(fakeEslint(() => [{ messages: [], errorCount: 0, warningCount: 0 }])))
|
|
63
|
+
await run(neostandardLint({ fix: false, ts: true }, loadEslint), [makeFile(Buffer.from('x'))])
|
|
64
|
+
expect(loadEslint).toHaveBeenCalledWith({ cwd: '/project', ts: true, fix: false, rules: {} })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('passes rule overrides through to the loader', async () => {
|
|
68
|
+
expect.assertions(1)
|
|
69
|
+
const loadEslint = jest.fn(() => Promise.resolve(fakeEslint(() => [{ messages: [], errorCount: 0, warningCount: 0 }])))
|
|
70
|
+
const rules = { 'no-template-curly-in-string': 'off' }
|
|
71
|
+
await run(neostandardLint({ rules }, loadEslint), [makeFile(Buffer.from('x'))])
|
|
72
|
+
expect(loadEslint).toHaveBeenCalledWith({ cwd: '/project', ts: false, fix: true, rules })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('passes a file through untouched when the project ignores it (ESLint returns no results)', async () => {
|
|
76
|
+
expect.assertions(2)
|
|
77
|
+
const eslint = fakeEslint(() => [])
|
|
78
|
+
const [linted] = await run(neostandardLint({}, () => Promise.resolve(eslint)), [makeFile(Buffer.from('var x=1'))])
|
|
79
|
+
expect(linted.contents.toString()).toEqual('var x=1')
|
|
80
|
+
expect(logSpy).toHaveBeenCalledWith('✖ Errors total: 0')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('skips null-content files without linting them', async () => {
|
|
84
|
+
expect.assertions(2)
|
|
85
|
+
const eslint = fakeEslint(() => [])
|
|
86
|
+
const [passed] = await run(neostandardLint({}, () => Promise.resolve(eslint)), [makeFile(null)])
|
|
87
|
+
expect(passed.isNull()).toBeTruthy()
|
|
88
|
+
expect(eslint.lintText).not.toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('errors on streaming file contents, which cannot be linted', async () => {
|
|
92
|
+
expect.assertions(1)
|
|
93
|
+
const streamFile = makeFile(Readable.from(['x']))
|
|
94
|
+
await expect(run(neostandardLint({}, () => Promise.resolve(fakeEslint(() => []))), [streamFile])).rejects.toThrow(/streaming/)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('prints per-file problems and a summary of totals across all files, without failing', async () => {
|
|
98
|
+
expect.assertions(5)
|
|
99
|
+
const eslint = fakeEslint((text, { filePath }) => filePath.endsWith('a.js')
|
|
100
|
+
? [{ messages: [{ line: 2, column: 7, message: 'unused b' }, { line: 3, column: 1, message: 'bad thing' }], errorCount: 2, warningCount: 0 }]
|
|
101
|
+
: [{ messages: [{ line: 1, column: 1, message: 'just a warning' }], errorCount: 0, warningCount: 1 }])
|
|
102
|
+
const out = await run(
|
|
103
|
+
neostandardLint({}, () => Promise.resolve(eslint)),
|
|
104
|
+
[makeFile(Buffer.from('a'), '/project/src/a.js'), makeFile(Buffer.from('b'), '/project/src/b.js')]
|
|
105
|
+
)
|
|
106
|
+
expect(out).toHaveLength(2)
|
|
107
|
+
expect(logSpy).toHaveBeenCalledWith('src/a.js')
|
|
108
|
+
expect(logSpy).toHaveBeenCalledWith('line 2:7\tunused b')
|
|
109
|
+
expect(logSpy).toHaveBeenCalledWith('✖ Errors total: 2')
|
|
110
|
+
expect(logSpy).toHaveBeenCalledWith('⚠ Warnings total: 1\n')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('surfaces a failure to load or run the linter as a stream error', async () => {
|
|
114
|
+
expect.assertions(1)
|
|
115
|
+
await expect(run(neostandardLint({}, () => Promise.reject(new Error('cannot load'))), [makeFile(Buffer.from('x'))])).rejects.toThrow('cannot load')
|
|
116
|
+
})
|
|
117
|
+
})
|
package/functions/partials.mjs
CHANGED
|
@@ -17,10 +17,10 @@ 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 { minifyFor } from './partials/minifyFor.mjs'
|
|
20
|
+
export { neostandardLint } from './partials/neostandardLint.mjs'
|
|
20
21
|
export { readmeTemplate } from './partials/readmeTemplate.mjs'
|
|
21
22
|
export { removeDirectory } from './partials/removeDirectory.mjs'
|
|
22
23
|
export { runOnChange } from './partials/runOnChange.mjs'
|
|
23
24
|
export { sassFor } from './partials/sassFor.mjs'
|
|
24
|
-
export { standardLint } from './partials/standardLint.mjs'
|
|
25
25
|
export { tsFor } from './partials/tsFor.mjs'
|
|
26
26
|
export { typeDocsFor } from './partials/typeDocsFor.mjs'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as gulpConfig from '../gulp.config.mjs'
|
|
2
|
+
import { dest, src } from 'gulp'
|
|
3
|
+
import { neostandardLint } from './partials/neostandardLint.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies neostandard linting (js-build-tools' chosen style) to the real, hand-written source - not the compiled
|
|
7
|
+
* dist/browser output, which is generated and so was never going to pass a hand-authored-code style check (every
|
|
8
|
+
* compiler/bundler emits its own boilerplate - import aliasing, interop headers - that only looks like a style
|
|
9
|
+
* violation). Includes test files: there is no reason to hold tests to a lower style bar than the rest of the
|
|
10
|
+
* source. When typescript.enabled, the real TypeScript source is linted too, with type-aware rules.
|
|
11
|
+
* @memberOf module:js-build-tools
|
|
12
|
+
* @returns {*}
|
|
13
|
+
*/
|
|
14
|
+
export const sourceLint = () => {
|
|
15
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
16
|
+
const ts = gulpConfig.get('typescript.enabled')
|
|
17
|
+
const patterns = [`${srcPath}/**/*.js`]
|
|
18
|
+
if (ts) {
|
|
19
|
+
patterns.push(`${srcPath}/**/*.ts`)
|
|
20
|
+
}
|
|
21
|
+
return src(patterns)
|
|
22
|
+
.pipe(neostandardLint({ fix: true, ts, rules: gulpConfig.get('lint.rules') }))
|
|
23
|
+
.pipe(dest(srcPath))
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import * as setUp from './test-helpers/setUp.mjs'
|
|
3
|
+
import { sourceLint } from './sourceLint.mjs'
|
|
4
|
+
import { countMatches } from 'test-filesystem'
|
|
5
|
+
|
|
6
|
+
// Run the real neostandard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
|
|
7
|
+
jest.mock('./partials/neostandardLint.mjs', () => require('./test-helpers/neostandardLintWithRealNeostandard.mjs'))
|
|
8
|
+
|
|
9
|
+
setUp.setDefaults('test-source-lint')
|
|
10
|
+
const gulpConfig = setUp.gulpConfig
|
|
11
|
+
|
|
12
|
+
const lintableContents = 'const x = "a";\n' +
|
|
13
|
+
'\n' +
|
|
14
|
+
'module.exports = x;\n'
|
|
15
|
+
|
|
16
|
+
const lintableTestContents = 'const y = "b";\n' +
|
|
17
|
+
'\n' +
|
|
18
|
+
'module.exports = y;\n'
|
|
19
|
+
|
|
20
|
+
beforeEach(
|
|
21
|
+
() => setUp.beforeEach()
|
|
22
|
+
.then(
|
|
23
|
+
async () => {
|
|
24
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
25
|
+
await fs.mkdirSync(srcPath, { recursive: true })
|
|
26
|
+
fs.writeFileSync(`${srcPath}/example.js`, lintableContents)
|
|
27
|
+
return fs.writeFileSync(`${srcPath}/example.test.js`, lintableTestContents)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
afterEach(setUp.afterEach)
|
|
33
|
+
|
|
34
|
+
describe('sourceLint', () => {
|
|
35
|
+
test('fixes the real source in place, including test files', done => {
|
|
36
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
37
|
+
expect.assertions(4)
|
|
38
|
+
sourceLint()
|
|
39
|
+
.on('finish', () => {
|
|
40
|
+
const linted = fs.readFileSync(`${srcPath}/example.js`).toString()
|
|
41
|
+
expect(countMatches(linted, '"')).toEqual(0)
|
|
42
|
+
expect(countMatches(linted, ';')).toEqual(0)
|
|
43
|
+
const lintedTest = fs.readFileSync(`${srcPath}/example.test.js`).toString()
|
|
44
|
+
expect(countMatches(lintedTest, '"')).toEqual(0)
|
|
45
|
+
expect(countMatches(lintedTest, ';')).toEqual(0)
|
|
46
|
+
done()
|
|
47
|
+
})
|
|
48
|
+
.on('error', error => {
|
|
49
|
+
console.error('Encountered error', error)
|
|
50
|
+
done(error)
|
|
51
|
+
})
|
|
52
|
+
}, 20000)
|
|
53
|
+
|
|
54
|
+
test('lint.rules actually suppresses a real neostandard finding', done => {
|
|
55
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
56
|
+
// A fixture string containing literal ${...} text (simulating real module source, not an accidental
|
|
57
|
+
// non-template string) - neostandard's no-template-curly-in-string rule flags this as an error by default.
|
|
58
|
+
const fixtureContents = 'const fixture = \'import(`imagemin-${pluginName}`)\'\n\nmodule.exports = fixture\n'
|
|
59
|
+
fs.writeFileSync(`${srcPath}/fixture.js`, fixtureContents)
|
|
60
|
+
gulpConfig.set('lint.rules', { 'no-template-curly-in-string': 'off' })
|
|
61
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
62
|
+
expect.assertions(1)
|
|
63
|
+
sourceLint()
|
|
64
|
+
.on('finish', () => {
|
|
65
|
+
gulpConfig.set('lint.rules', {})
|
|
66
|
+
expect(logSpy.mock.calls.flat().join('\n')).not.toMatch(/no-template-curly-in-string|template string expression/)
|
|
67
|
+
logSpy.mockRestore()
|
|
68
|
+
done()
|
|
69
|
+
})
|
|
70
|
+
.on('error', error => {
|
|
71
|
+
gulpConfig.set('lint.rules', {})
|
|
72
|
+
logSpy.mockRestore()
|
|
73
|
+
console.error('Encountered error', error)
|
|
74
|
+
done(error)
|
|
75
|
+
})
|
|
76
|
+
}, 20000)
|
|
77
|
+
|
|
78
|
+
test('also lints .ts source when typescript.enabled', done => {
|
|
79
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
80
|
+
gulpConfig.set('typescript.enabled', true)
|
|
81
|
+
fs.writeFileSync(`${srcPath}/example.ts`, 'const z: string = "c";\n\nexport default z;\n')
|
|
82
|
+
expect.assertions(2)
|
|
83
|
+
sourceLint()
|
|
84
|
+
.on('finish', () => {
|
|
85
|
+
gulpConfig.set('typescript.enabled', false)
|
|
86
|
+
const linted = fs.readFileSync(`${srcPath}/example.ts`).toString()
|
|
87
|
+
expect(countMatches(linted, '"')).toEqual(0)
|
|
88
|
+
expect(countMatches(linted, ';')).toEqual(0)
|
|
89
|
+
done()
|
|
90
|
+
})
|
|
91
|
+
.on('error', error => {
|
|
92
|
+
gulpConfig.set('typescript.enabled', false)
|
|
93
|
+
console.error('Encountered error', error)
|
|
94
|
+
done(error)
|
|
95
|
+
})
|
|
96
|
+
}, 20000)
|
|
97
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
// Jest's CommonJS transform cannot load `neostandard` (ESM-only) or run a real dynamic import(), so tests that need
|
|
6
|
+
// genuine neostandard behavior run the actual package in a plain Node subprocess instead of faking it.
|
|
7
|
+
const script = 'import { readFileSync } from \'node:fs\'\n' +
|
|
8
|
+
'import { ESLint } from \'eslint\'\n' +
|
|
9
|
+
'import { neostandard, defaultFilePatterns } from \'neostandard\'\n' +
|
|
10
|
+
'const { text, filePath, cwd, fix, ts, env, rules } = JSON.parse(readFileSync(0, \'utf8\'))\n' +
|
|
11
|
+
'const baseConfig = neostandard({ ts, env })\n' +
|
|
12
|
+
'if (Object.keys(rules).length) { baseConfig.push({ files: defaultFilePatterns.all, rules }) }\n' +
|
|
13
|
+
'const eslint = new ESLint({ cwd, fix, overrideConfigFile: true, baseConfig })\n' +
|
|
14
|
+
'process.stdout.write(JSON.stringify(await eslint.lintText(text, { filePath })))\n'
|
|
15
|
+
|
|
16
|
+
const readEnv = cwd => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).standard?.env ?? []
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const loadEslintViaSubprocess = ({ cwd, ts, fix, rules = {} }) => Promise.resolve({
|
|
25
|
+
lintText: (text, { filePath }) => Promise.resolve().then(() => JSON.parse(execFileSync(
|
|
26
|
+
process.execPath,
|
|
27
|
+
['--input-type=module', '--eval', script],
|
|
28
|
+
{
|
|
29
|
+
input: JSON.stringify({ text, filePath, cwd, fix, ts, env: readEnv(cwd), rules }),
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
cwd,
|
|
32
|
+
maxBuffer: 256 * 1024 * 1024
|
|
33
|
+
}
|
|
34
|
+
)))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const actual = jest.requireActual('../partials/neostandardLint.mjs')
|
|
38
|
+
|
|
39
|
+
// Drop-in replacement for the partial module (see jest.mock in the lint/build tests): same API, real neostandard.
|
|
40
|
+
export const neostandardLint = (options) => actual.neostandardLint(options, loadEslintViaSubprocess)
|
|
@@ -46,7 +46,9 @@ const performDotNotate = (arrayObject, didRetain, prepend = '', results = {}) =>
|
|
|
46
46
|
if (didRetain(currentKey, value, results)) {
|
|
47
47
|
continue
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// An empty object/array has no keys to flatten into - recursing into one would silently drop currentKey
|
|
50
|
+
// from the result entirely, rather than keeping it as the empty value it actually is. Treat it as a leaf.
|
|
51
|
+
if (isObject(value) && Object.keys(value).length) {
|
|
50
52
|
performDotNotate(value, didRetain, `${currentKey}.`, results)
|
|
51
53
|
continue
|
|
52
54
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { dotNotate } from './dotNotate.mjs'
|
|
2
|
+
|
|
3
|
+
describe('dotNotate', () => {
|
|
4
|
+
test('flattens a nested object into dot-notation leaves', () => {
|
|
5
|
+
expect(dotNotate({ a: { b: 'c', d: { e: 'f' } } })).toEqual({ 'a.b': 'c', 'a.d.e': 'f' })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('flattens a non-empty array into index-numbered leaves, same as a plain object', () => {
|
|
9
|
+
expect(dotNotate({ a: ['x', 'y'] })).toEqual({ 'a.0': 'x', 'a.1': 'y' })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('keeps a primitive value (including null) as its own leaf, unflattened', () => {
|
|
13
|
+
expect(dotNotate({ a: 1, b: 'two', c: null })).toEqual({ a: 1, b: 'two', c: null })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('keeps an empty object as a leaf value, rather than silently dropping its key', () => {
|
|
17
|
+
expect(dotNotate({ a: {}, b: 'c' })).toEqual({ a: {}, b: 'c' })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('keeps an empty array as a leaf value, rather than silently dropping its key', () => {
|
|
21
|
+
expect(dotNotate({ a: [], b: 'c' })).toEqual({ a: [], b: 'c' })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('keeps an empty object/array as a leaf even nested inside other objects', () => {
|
|
25
|
+
expect(dotNotate({ a: { b: [], c: {} } })).toEqual({ 'a.b': [], 'a.c': {} })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('retains a matching key as a whole object instead of flattening into it', () => {
|
|
29
|
+
expect(dotNotate({ a: { b: 'c' }, options: { fix: true } }, ['options'])).toEqual({
|
|
30
|
+
'a.b': 'c',
|
|
31
|
+
options: { fix: true }
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -16,7 +16,7 @@ export const getBundleConfigs = () => {
|
|
|
16
16
|
ignore: gulpConfig.get('browser.ignore'),
|
|
17
17
|
exclude: gulpConfig.get('browser.exclude')
|
|
18
18
|
}
|
|
19
|
-
const additional = gulpConfig.get('browser.bundles')
|
|
19
|
+
const additional = gulpConfig.get('browser.bundles')
|
|
20
20
|
return [
|
|
21
21
|
primary,
|
|
22
22
|
...additional.map((bundleConfig) => ({
|