js-build-tools 6.0.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.
Files changed (52) hide show
  1. package/README.md +45 -2
  2. package/dist/build.js +63 -62
  3. package/dist/build.min.js +1 -1
  4. package/dist/bundle.js +40 -39
  5. package/dist/bundle.min.js +1 -1
  6. package/dist/bundleMinify.js +18 -17
  7. package/dist/bundleMinify.min.js +1 -1
  8. package/dist/copyFonts.js +13 -12
  9. package/dist/copyFonts.min.js +1 -1
  10. package/dist/default.js +15 -14
  11. package/dist/default.min.js +1 -1
  12. package/dist/distMinify.js +13 -12
  13. package/dist/distMinify.min.js +1 -1
  14. package/dist/partials/addToReadme.js +25 -24
  15. package/dist/partials/beginWatcher.js +13 -12
  16. package/dist/partials/clean.js +15 -14
  17. package/dist/partials/distFor.js +22 -21
  18. package/dist/partials/distForSrc.js +12 -11
  19. package/dist/partials/distSeries.js +20 -19
  20. package/dist/partials/imagesFor.js +43 -42
  21. package/dist/partials/neostandardLint.js +130 -111
  22. package/dist/partials/readmeTemplate.js +15 -14
  23. package/dist/partials/runOnChange.js +29 -28
  24. package/dist/partials/sassFor.js +29 -28
  25. package/dist/partials/tsFor.js +71 -70
  26. package/dist/partials/typeDocsFor.js +203 -202
  27. package/dist/sourceLint.js +26 -24
  28. package/dist/sourceLint.min.js +1 -1
  29. package/dist/test-helpers/neostandardLintWithRealNeostandard.js +49 -47
  30. package/dist/test-helpers/setUp.js +95 -94
  31. package/dist/testFull.js +20 -19
  32. package/dist/testFull.min.js +1 -1
  33. package/dist/testHelpers.js +11 -10
  34. package/dist/testHelpers.min.js +1 -1
  35. package/dist/testQuick.js +22 -21
  36. package/dist/testQuick.min.js +1 -1
  37. package/dist/utilities/dotNotate.js +48 -46
  38. package/dist/utilities/getBundleConfigs.js +28 -27
  39. package/dist/watchTest.js +16 -15
  40. package/dist/watchTest.min.js +1 -1
  41. package/functions/bundle.mjs +2 -2
  42. package/functions/partials/addToReadme.mjs +1 -1
  43. package/functions/partials/neostandardLint.mjs +19 -7
  44. package/functions/partials/neostandardLint.test.mjs +9 -1
  45. package/functions/sourceLint.mjs +1 -1
  46. package/functions/sourceLint.test.mjs +24 -0
  47. package/functions/test-helpers/neostandardLintWithRealNeostandard.mjs +7 -5
  48. package/functions/utilities/dotNotate.mjs +3 -1
  49. package/functions/utilities/dotNotate.test.mjs +34 -0
  50. package/functions/utilities/getBundleConfigs.mjs +1 -1
  51. package/gulp.config.mjs +14 -0
  52. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { importModule } from './loadPeer.mjs'
6
6
 
7
7
  // neostandard (js-build-tools' chosen style - a maintained, TypeScript-aware successor to `standard`, see
8
8
  // https://github.com/neostandard/neostandard) ships ESM-only, see importModule for how it is imported.
9
- const importNeostandard = () => importModule('neostandard').then(({ neostandard }) => neostandard)
9
+ const importNeostandard = () => importModule('neostandard')
10
10
 
11
11
  /**
12
12
  * Read the `env` a project already declared for Standard (the `standard` field in its package.json), so a project
@@ -23,20 +23,30 @@ const readEnv = cwd => {
23
23
  }
24
24
 
25
25
  /**
26
- * Build an ESLint instance configured with neostandard, honoring the project's own package.json `standard.env`.
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.
27
32
  * @param {Object} options
28
33
  * @param {string} options.cwd
29
34
  * @param {boolean} options.ts
30
35
  * @param {boolean} options.fix
36
+ * @param {Object<string, *>} [options.rules={}]
31
37
  * @returns {Promise<import('eslint').ESLint>}
32
38
  */
33
- const buildEslint = async ({ cwd, ts, fix }) => {
34
- const neostandard = await importNeostandard()
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
+ }
35
45
  return new ESLint({
36
46
  cwd,
37
47
  fix,
38
48
  overrideConfigFile: true,
39
- baseConfig: neostandard({ ts, env: readEnv(cwd) })
49
+ baseConfig
40
50
  })
41
51
  }
42
52
 
@@ -67,13 +77,15 @@ const printFileReport = (file, result) => {
67
77
  * @param {Object} [options={}]
68
78
  * @param {boolean} [options.fix=true] - Apply neostandard's automatic fixes to the file contents.
69
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`).
70
82
  * @param {function(Object): Promise<{lintText: Function}>} [loadEslint] - Resolves an ESLint-shaped linter. Real
71
83
  * callers should never need to pass this - it exists so tests can supply a stand-in, since this project's
72
84
  * CommonJS-transform-based Jest setup cannot execute a real dynamic import() of neostandard (ESM-only).
73
85
  * @returns {import('stream').Transform}
74
86
  */
75
87
  export const neostandardLint = (
76
- { fix = true, ts = false } = {},
88
+ { fix = true, ts = false, rules = {} } = {},
77
89
  loadEslint = buildEslint
78
90
  ) => {
79
91
  const totals = { errors: 0, warnings: 0 }
@@ -89,7 +101,7 @@ export const neostandardLint = (
89
101
  return
90
102
  }
91
103
  if (!eslintPromise) {
92
- eslintPromise = loadEslint({ cwd: file.cwd, ts, fix })
104
+ eslintPromise = loadEslint({ cwd: file.cwd, ts, fix, rules })
93
105
  }
94
106
  eslintPromise
95
107
  .then(eslint => eslint.lintText(file.contents.toString(), { filePath: file.path }))
@@ -61,7 +61,15 @@ describe('neostandardLint', () => {
61
61
  expect.assertions(1)
62
62
  const loadEslint = jest.fn(() => Promise.resolve(fakeEslint(() => [{ messages: [], errorCount: 0, warningCount: 0 }])))
63
63
  await run(neostandardLint({ fix: false, ts: true }, loadEslint), [makeFile(Buffer.from('x'))])
64
- expect(loadEslint).toHaveBeenCalledWith({ cwd: '/project', ts: true, fix: false })
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 })
65
73
  })
66
74
 
67
75
  test('passes a file through untouched when the project ignores it (ESLint returns no results)', async () => {
@@ -19,6 +19,6 @@ export const sourceLint = () => {
19
19
  patterns.push(`${srcPath}/**/*.ts`)
20
20
  }
21
21
  return src(patterns)
22
- .pipe(neostandardLint({ fix: true, ts }))
22
+ .pipe(neostandardLint({ fix: true, ts, rules: gulpConfig.get('lint.rules') }))
23
23
  .pipe(dest(srcPath))
24
24
  }
@@ -51,6 +51,30 @@ describe('sourceLint', () => {
51
51
  })
52
52
  }, 20000)
53
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
+
54
78
  test('also lints .ts source when typescript.enabled', done => {
55
79
  const srcPath = gulpConfig.get('srcPath')
56
80
  gulpConfig.set('typescript.enabled', true)
@@ -6,9 +6,11 @@ import path from 'node:path'
6
6
  // genuine neostandard behavior run the actual package in a plain Node subprocess instead of faking it.
7
7
  const script = 'import { readFileSync } from \'node:fs\'\n' +
8
8
  'import { ESLint } from \'eslint\'\n' +
9
- 'import { neostandard } from \'neostandard\'\n' +
10
- 'const { text, filePath, cwd, fix, ts, env } = JSON.parse(readFileSync(0, \'utf8\'))\n' +
11
- 'const eslint = new ESLint({ cwd, fix, overrideConfigFile: true, baseConfig: neostandard({ ts, env }) })\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' +
12
14
  'process.stdout.write(JSON.stringify(await eslint.lintText(text, { filePath })))\n'
13
15
 
14
16
  const readEnv = cwd => {
@@ -19,12 +21,12 @@ const readEnv = cwd => {
19
21
  }
20
22
  }
21
23
 
22
- export const loadEslintViaSubprocess = ({ cwd, ts, fix }) => Promise.resolve({
24
+ export const loadEslintViaSubprocess = ({ cwd, ts, fix, rules = {} }) => Promise.resolve({
23
25
  lintText: (text, { filePath }) => Promise.resolve().then(() => JSON.parse(execFileSync(
24
26
  process.execPath,
25
27
  ['--input-type=module', '--eval', script],
26
28
  {
27
- input: JSON.stringify({ text, filePath, cwd, fix, ts, env: readEnv(cwd) }),
29
+ input: JSON.stringify({ text, filePath, cwd, fix, ts, env: readEnv(cwd), rules }),
28
30
  encoding: 'utf8',
29
31
  cwd,
30
32
  maxBuffer: 256 * 1024 * 1024
@@ -46,7 +46,9 @@ const performDotNotate = (arrayObject, didRetain, prepend = '', results = {}) =>
46
46
  if (didRetain(currentKey, value, results)) {
47
47
  continue
48
48
  }
49
- if (isObject(value)) {
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) => ({
package/gulp.config.mjs CHANGED
@@ -96,6 +96,16 @@ import { fileExists } from 'test-filesystem'
96
96
  * @property {StringSetting} from - Path to search for images.
97
97
  * @property {StringSetting} to - Path to output images.
98
98
  */
99
+ /**
100
+ * Configurations for linting (see {@link module:partials.neostandardLint}).
101
+ * @typedef {Object<string, Setting>} module:gulpConfig~LintConfig
102
+ * @property {Object<string, *>} rules - ESLint rule overrides layered on top of neostandard's own config, scoped
103
+ * to the same files neostandard itself lints. For a rule that is wrong for your project specifically (for example
104
+ * a test fixture that deliberately contains text neostandard mistakes for a real syntax problem), rather than
105
+ * something to fix in your code. Empty by default: a project that never sets this gets exactly neostandard's own
106
+ * rules, unchanged. Keyed and valued exactly as ESLint's own `rules` object - see
107
+ * https://eslint.org/docs/latest/use/configure/rules.
108
+ */
99
109
  /**
100
110
  * Configurations to compile and generate the Readme file.
101
111
  * @typedef {Object<string, Setting>} module:gulpConfig~ReadmeConfig
@@ -153,6 +163,7 @@ import { fileExists } from 'test-filesystem'
153
163
  * @property {DocsConfig} docs - Generate the documentation of a TypeScript project configuration group.
154
164
  * @property {FontConfig} fonts - Fonts copy configuration group.
155
165
  * @property {ImageConfig} images - Minify and copy the images configuration.
166
+ * @property {LintConfig} lint - Linting configuration group.
156
167
  * @property {ReadmeConfig} readme - Build readme files configuration.
157
168
  * @property {StringSetting} rootPath - Base directory of the project.
158
169
  * @property {SassConfig} sass - Compile CSS from SASS configuration.
@@ -200,6 +211,9 @@ const setDefaults = {
200
211
  from: 'src/img/**/*.+(png|jpg|jpeg|gif|svg)',
201
212
  to: 'browser/img',
202
213
  },
214
+ lint: {
215
+ rules: {},
216
+ },
203
217
  readme: {
204
218
  file: 'README.md',
205
219
  from: 'src/**/!(*.test).js',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-build-tools",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Standardised build process with gulp.",
5
5
  "main": "gulpfile.base.js",
6
6
  "exports": {