js-build-tools 4.1.1 → 5.0.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 +274 -267
- package/dist/build.js +58 -56
- package/dist/build.min.js +1 -1
- package/dist/bundle.js +16 -16
- package/dist/bundleLint.js +16 -20
- package/dist/bundleLint.min.js +1 -1
- package/dist/bundleMinify.js +13 -13
- package/dist/compileReadme.js +10 -10
- package/dist/copyFonts.js +13 -13
- package/dist/default.js +15 -15
- package/dist/dist.js +10 -10
- package/dist/distLint.js +16 -20
- package/dist/distLint.min.js +1 -1
- package/dist/distMinify.js +13 -13
- package/dist/images.js +10 -10
- package/dist/partials/copyFor.js +12 -9
- package/dist/partials/imagesFor.js +56 -21
- package/dist/partials/removeDirectory.js +37 -13
- package/dist/partials/sassFor.js +8 -2
- package/dist/partials/standardLint.js +96 -0
- package/dist/partials/tsFor.js +67 -67
- package/dist/partials.js +110 -103
- package/dist/partials.min.js +1 -1
- package/dist/sass.js +10 -10
- package/dist/test-helpers/standardLintWithRealStandard.js +28 -0
- package/dist/testFull.js +3 -3
- package/dist/testHelpers.js +10 -10
- package/dist/testQuick.js +3 -3
- package/dist/typeScript.js +9 -9
- package/dist/watchFull.js +10 -10
- package/dist/watchTest.js +16 -16
- package/functions/build.mjs +6 -3
- package/functions/build.test.mjs +31 -0
- package/functions/bundleLint.mjs +2 -6
- package/functions/bundleLint.test.mjs +3 -0
- package/functions/distLint.mjs +2 -6
- package/functions/distLint.test.mjs +3 -0
- package/functions/partials/copyFor.mjs +2 -1
- package/functions/partials/copyFor.test.mjs +18 -0
- package/functions/partials/imagesFor.mjs +35 -3
- package/functions/partials/imagesFor.test.mjs +83 -1
- package/functions/partials/removeDirectory.mjs +28 -14
- package/functions/partials/removeDirectory.test.mjs +44 -0
- package/functions/partials/sassFor.mjs +5 -2
- package/functions/partials/sassFor.test.mjs +22 -4
- package/functions/partials/standardLint.mjs +80 -0
- package/functions/partials/standardLint.test.mjs +99 -0
- package/functions/partials.mjs +1 -0
- package/functions/partials.test.mjs +1 -1
- package/functions/test-helpers/standardLintWithRealStandard.mjs +21 -0
- package/package.json +15 -4
package/dist/testQuick.js
CHANGED
|
@@ -5,9 +5,9 @@ Object.defineProperty(exports, '__esModule', {
|
|
|
5
5
|
value: true
|
|
6
6
|
})
|
|
7
7
|
exports.testQuick = void 0
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap()
|
|
8
|
+
const gulpConfig = _interopRequireWildcard(require('../gulp.config.js'))
|
|
9
|
+
const _jest = require('jest')
|
|
10
|
+
function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; let o; let i; const f = { __proto__: null, default: e }; if (e === null || typeof e !== 'object' && typeof e !== 'function') return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f) } for (const t in e) t !== 'default' && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f })(e, t) }
|
|
11
11
|
/**
|
|
12
12
|
* Run the Jest tests for files which have been modified (based on git status).
|
|
13
13
|
* Configure where tests are located by using 'testPath'.
|
package/dist/typeScript.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', {
|
|
4
|
-
value: true
|
|
5
|
-
})
|
|
6
|
-
exports.typeScript = void 0
|
|
7
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', {
|
|
4
|
+
value: true
|
|
5
|
+
})
|
|
6
|
+
exports.typeScript = void 0
|
|
7
|
+
const _tsFor = require('./partials/tsFor.js')
|
|
8
8
|
/**
|
|
9
9
|
* Simplified typescript task using tsFor.
|
|
10
10
|
* @memberOf module:js-build-tools
|
|
11
11
|
* @returns {Function}
|
|
12
|
-
*/
|
|
13
|
-
const typeScript = exports.typeScript = (0, _tsFor.tsFor)()
|
|
12
|
+
*/
|
|
13
|
+
const typeScript = exports.typeScript = (0, _tsFor.tsFor)()
|
package/dist/watchFull.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, '__esModule', {
|
|
4
|
-
value: true
|
|
5
|
-
})
|
|
6
|
-
exports.watchFull = void 0
|
|
7
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', {
|
|
4
|
+
value: true
|
|
5
|
+
})
|
|
6
|
+
exports.watchFull = void 0
|
|
7
|
+
const _partials = require('./partials.js')
|
|
8
8
|
/**
|
|
9
9
|
* Watch for changes and run the distribution for the changed files, then bundle and test the changed files.
|
|
10
10
|
* @memberOf module:js-build-tools
|
|
11
11
|
* @returns {FSWatcher}
|
|
12
|
-
*/
|
|
13
|
-
const watchFull = () => (0, _partials.beginWatcher)().on('change', _partials.runOnChange)
|
|
14
|
-
exports.watchFull = watchFull
|
|
12
|
+
*/
|
|
13
|
+
const watchFull = () => (0, _partials.beginWatcher)().on('change', _partials.runOnChange)
|
|
14
|
+
exports.watchFull = watchFull
|
package/dist/watchTest.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
require('core-js/modules/esnext.weak-map.delete-all.js')
|
|
4
|
-
Object.defineProperty(exports, '__esModule', {
|
|
5
|
-
value: true
|
|
6
|
-
})
|
|
7
|
-
exports.watchTest = void 0
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap()
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
require('core-js/modules/esnext.weak-map.delete-all.js')
|
|
4
|
+
Object.defineProperty(exports, '__esModule', {
|
|
5
|
+
value: true
|
|
6
|
+
})
|
|
7
|
+
exports.watchTest = void 0
|
|
8
|
+
const gulpConfig = _interopRequireWildcard(require('../gulp.config.js'))
|
|
9
|
+
const _gulp = require('gulp')
|
|
10
|
+
const _testQuick = require('./testQuick.js')
|
|
11
|
+
function _interopRequireWildcard (e, t) { if (typeof WeakMap === 'function') var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; let o; let i; const f = { __proto__: null, default: e }; if (e === null || typeof e !== 'object' && typeof e !== 'function') return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f) } for (const t in e) t !== 'default' && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f })(e, t) }
|
|
12
12
|
/**
|
|
13
13
|
* Watch for changes and run the tests.
|
|
14
14
|
* @memberOf module:js-build-tools
|
|
15
15
|
* @returns {*}
|
|
16
|
-
*/
|
|
17
|
-
const watchTest = () => (0, _gulp.watch)(gulpConfig.get('test.watch'), {
|
|
18
|
-
ignoreInitial: false
|
|
19
|
-
}, _testQuick.testQuick)
|
|
20
|
-
exports.watchTest = watchTest
|
|
16
|
+
*/
|
|
17
|
+
const watchTest = () => (0, _gulp.watch)(gulpConfig.get('test.watch'), {
|
|
18
|
+
ignoreInitial: false
|
|
19
|
+
}, _testQuick.testQuick)
|
|
20
|
+
exports.watchTest = watchTest
|
package/functions/build.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { testFull } from './testFull.mjs'
|
|
|
21
21
|
export const build = (done = null) => {
|
|
22
22
|
const distLintMinify = parallel(distLint, distMinify)
|
|
23
23
|
const bundleLintMinify = parallel(bundleLint, bundleMinify)
|
|
24
|
-
const buildActions = [
|
|
24
|
+
const buildActions = [distSeries(), distLintMinify]
|
|
25
25
|
if (gulpConfig.get('typescript.enabled')) {
|
|
26
26
|
// For ts usage, we need to run the readme on the dist directly since that is where the .js files are located
|
|
27
27
|
buildActions.push(compileReadme)
|
|
@@ -51,7 +51,10 @@ export const build = (done = null) => {
|
|
|
51
51
|
// Conditionally add SASS process
|
|
52
52
|
runActions.push(sass)
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// clean has to finish before anything writes into the output folders - images, fonts and sass all write into
|
|
55
|
+
// browser/, so running clean in parallel with them races (ENOTEMPTY errors from clean, or output deleted mid-write).
|
|
56
|
+
return series(
|
|
57
|
+
clean,
|
|
58
|
+
parallel(...runActions)
|
|
56
59
|
)(done)
|
|
57
60
|
}
|
package/functions/build.test.mjs
CHANGED
|
@@ -5,6 +5,9 @@ import { clean } from './partials/clean.mjs'
|
|
|
5
5
|
import { testFull } from './testFull.mjs'
|
|
6
6
|
import { countMatches } from 'test-filesystem'
|
|
7
7
|
|
|
8
|
+
// Run the real Standard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
|
|
9
|
+
jest.mock('./partials/standardLint.mjs', () => require('./test-helpers/standardLintWithRealStandard.mjs'))
|
|
10
|
+
|
|
8
11
|
setUp.setDefaults('test-build')
|
|
9
12
|
const gulpConfig = setUp.gulpConfig
|
|
10
13
|
|
|
@@ -621,4 +624,32 @@ describe('build', () => {
|
|
|
621
624
|
done()
|
|
622
625
|
})
|
|
623
626
|
}, 30000)
|
|
627
|
+
|
|
628
|
+
test('finishes clean before starting the tasks that write into the output folders', done => {
|
|
629
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
630
|
+
const sourcesPath = `${srcPath}/sources`
|
|
631
|
+
fs.writeFileSync(`${sourcesPath}/file1.js`, file1Contents)
|
|
632
|
+
fs.writeFileSync(`${sourcesPath}/file2.js`, file2Contents)
|
|
633
|
+
fs.writeFileSync(`${srcPath}/main.js`, mainFileContents)
|
|
634
|
+
expect.assertions(1)
|
|
635
|
+
const events = []
|
|
636
|
+
clean.mockImplementationOnce(() => new Promise(resolve => setTimeout(() => {
|
|
637
|
+
events.push('clean finished')
|
|
638
|
+
resolve(true)
|
|
639
|
+
}, 100)))
|
|
640
|
+
testFull.mockImplementationOnce(() => {
|
|
641
|
+
events.push('testFull started')
|
|
642
|
+
return Promise.resolve(true)
|
|
643
|
+
})
|
|
644
|
+
// Earlier tests leave these enabled, so state them explicitly to keep this test independent of test order.
|
|
645
|
+
gulpConfig.set('browser.enabled', false)
|
|
646
|
+
gulpConfig.set('fonts.enabled', false)
|
|
647
|
+
gulpConfig.set('images.enabled', false)
|
|
648
|
+
gulpConfig.set('sass.enabled', false)
|
|
649
|
+
gulpConfig.set('typescript.enabled', false)
|
|
650
|
+
build(() => {
|
|
651
|
+
expect(events).toEqual(['clean finished', 'testFull started'])
|
|
652
|
+
done()
|
|
653
|
+
})
|
|
654
|
+
}, 30000)
|
|
624
655
|
})
|
package/functions/bundleLint.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dest, src } from 'gulp'
|
|
2
2
|
import * as gulpConfig from '../gulp.config.mjs'
|
|
3
|
-
import
|
|
3
|
+
import { standardLint } from './partials/standardLint.mjs'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Applies Standard code style linting to bundled file.
|
|
@@ -8,9 +8,5 @@ import standard from 'gulp-standard'
|
|
|
8
8
|
* @returns {stream.Stream}
|
|
9
9
|
*/
|
|
10
10
|
export const bundleLint = () => src(`${gulpConfig.get('browser.to')}/${gulpConfig.get('browser.name')}.js`)
|
|
11
|
-
.pipe(
|
|
12
|
-
.pipe(standard.reporter('default', {
|
|
13
|
-
fix: true,
|
|
14
|
-
quiet: true
|
|
15
|
-
}))
|
|
11
|
+
.pipe(standardLint({ fix: true }))
|
|
16
12
|
.pipe(dest(gulpConfig.get('browser.to')))
|
|
@@ -3,6 +3,9 @@ import * as setUp from './test-helpers/setUp.mjs'
|
|
|
3
3
|
import { bundleLint } from './bundleLint.mjs'
|
|
4
4
|
import { countMatches } from 'test-filesystem'
|
|
5
5
|
|
|
6
|
+
// Run the real Standard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
|
|
7
|
+
jest.mock('./partials/standardLint.mjs', () => require('./test-helpers/standardLintWithRealStandard.mjs'))
|
|
8
|
+
|
|
6
9
|
setUp.setDefaults('test-bundle-lint')
|
|
7
10
|
const gulpConfig = setUp.gulpConfig
|
|
8
11
|
|
package/functions/distLint.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as gulpConfig from '../gulp.config.mjs'
|
|
2
2
|
import { dest, src } from 'gulp'
|
|
3
|
-
import
|
|
3
|
+
import { standardLint } from './partials/standardLint.mjs'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Applies Standard code style linting to distribution files.
|
|
@@ -8,9 +8,5 @@ import standard from 'gulp-standard'
|
|
|
8
8
|
* @returns {*}
|
|
9
9
|
*/
|
|
10
10
|
export const distLint = () => src(gulpConfig.get('browser.from'))
|
|
11
|
-
.pipe(
|
|
12
|
-
.pipe(standard.reporter('default', {
|
|
13
|
-
fix: true,
|
|
14
|
-
quiet: true
|
|
15
|
-
}))
|
|
11
|
+
.pipe(standardLint({ fix: true }))
|
|
16
12
|
.pipe(dest(gulpConfig.get('dist.to')))
|
|
@@ -3,6 +3,9 @@ import * as setUp from './test-helpers/setUp.mjs'
|
|
|
3
3
|
import { distLint } from './distLint.mjs'
|
|
4
4
|
import { countMatches } from 'test-filesystem'
|
|
5
5
|
|
|
6
|
+
// Run the real Standard (in a subprocess) - Jest's CommonJS transform can't load the ESM-only package itself.
|
|
7
|
+
jest.mock('./partials/standardLint.mjs', () => require('./test-helpers/standardLintWithRealStandard.mjs'))
|
|
8
|
+
|
|
6
9
|
setUp.setDefaults('test-dist-lint')
|
|
7
10
|
const gulpConfig = setUp.gulpConfig
|
|
8
11
|
|
|
@@ -7,5 +7,6 @@ import { dest, src } from 'gulp'
|
|
|
7
7
|
* @param {string} destPath
|
|
8
8
|
* @returns {stream.Stream}
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
// encoding: false copies the bytes untouched - Gulp 5's default utf8 handling corrupts binary files such as fonts.
|
|
11
|
+
export const copyFor = (srcPath, destPath) => src(srcPath, { encoding: false })
|
|
11
12
|
.pipe(dest(destPath))
|
|
@@ -32,4 +32,22 @@ describe('copyFor', () => {
|
|
|
32
32
|
done()
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
|
+
|
|
36
|
+
test('copies binary files byte for byte, including a leading BOM and bytes above 0x7f', done => {
|
|
37
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
38
|
+
const binaryFile = `${srcPath}/font.woff`
|
|
39
|
+
const original = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(Array.from({ length: 512 }, (_, i) => i % 256))])
|
|
40
|
+
fs.writeFileSync(binaryFile, original)
|
|
41
|
+
expect.assertions(1)
|
|
42
|
+
const browserPath = gulpConfig.get('browser.to')
|
|
43
|
+
copyFor(binaryFile, browserPath)
|
|
44
|
+
.on('finish', () => {
|
|
45
|
+
expect(fs.readFileSync(`${browserPath}/font.woff`).equals(original)).toBeTruthy()
|
|
46
|
+
done()
|
|
47
|
+
})
|
|
48
|
+
.on('error', error => {
|
|
49
|
+
console.error('Encountered error', error)
|
|
50
|
+
done()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
35
53
|
})
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import cache from 'gulp-cache'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
2
3
|
import { dest, src } from 'gulp'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import * as gulpConfig from '../../gulp.config.mjs'
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
const installHint = 'Add it to your project with: npm install --save-dev github:jheagle/gulp-imagemin#common-js-compatibility ' +
|
|
8
|
+
'(the CommonJS-compatible fork - upstream gulp-imagemin is ESM-only and cannot be require()d by this package\'s CommonJS build).'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load `gulp-imagemin`, an optional peer dependency: image optimization is off by default and its binary-download
|
|
12
|
+
* dependency chain is heavy, so it is only required once the images task actually runs, and it is resolved from the
|
|
13
|
+
* consuming project's own directory rather than from this package.
|
|
14
|
+
* @memberOf module:partials
|
|
15
|
+
* @param {string} [projectPath=process.cwd()] - The project directory to resolve `gulp-imagemin` from.
|
|
16
|
+
* @returns {Function} The gulp-imagemin plugin factory.
|
|
17
|
+
* @throws {Error} With install instructions when gulp-imagemin is missing or is the ESM-only upstream release.
|
|
18
|
+
*/
|
|
19
|
+
export const loadImagemin = (projectPath = process.cwd()) => {
|
|
20
|
+
let loaded
|
|
21
|
+
try {
|
|
22
|
+
loaded = createRequire(path.join(projectPath, 'package.json'))('gulp-imagemin')
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code === 'MODULE_NOT_FOUND' && /Cannot find module 'gulp-imagemin'/.test(error.message)) {
|
|
25
|
+
throw new Error(`The images task needs the optional peer dependency gulp-imagemin, which is not installed. ${installHint}`)
|
|
26
|
+
}
|
|
27
|
+
if (error.code === 'ERR_REQUIRE_ESM') {
|
|
28
|
+
throw new Error(`gulp-imagemin could not be loaded because it is ESM-only. ${installHint}`)
|
|
29
|
+
}
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
return loaded && (loaded.__esModule || loaded.default) ? loaded.default : loaded
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
/**
|
|
7
36
|
* Move and optimize images into the browser directory.
|
|
@@ -10,8 +39,10 @@ import imagemin from 'gulp-imagemin'
|
|
|
10
39
|
* @param {string} [imageDest=dest/image/folder]
|
|
11
40
|
* @return {stream.Stream}
|
|
12
41
|
*/
|
|
13
|
-
export const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest = gulpConfig.get('images.to')) =>
|
|
14
|
-
|
|
42
|
+
export const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest = gulpConfig.get('images.to')) => {
|
|
43
|
+
const imagemin = loadImagemin()
|
|
44
|
+
// Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images (and makes gulp-cache drop them).
|
|
45
|
+
return src(imageSrc, { encoding: false })
|
|
15
46
|
.pipe(
|
|
16
47
|
// Caching images that ran through imagemin
|
|
17
48
|
cache(
|
|
@@ -23,3 +54,4 @@ export const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest =
|
|
|
23
54
|
)
|
|
24
55
|
)
|
|
25
56
|
.pipe(dest(imageDest))
|
|
57
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import * as setUp from '../test-helpers/setUp.mjs'
|
|
3
|
-
import
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import zlib from 'zlib'
|
|
5
|
+
import { imagesFor, loadImagemin } from './imagesFor.mjs'
|
|
4
6
|
|
|
5
7
|
setUp.setDefaults('test-images-for')
|
|
6
8
|
const gulpConfig = setUp.gulpConfig
|
|
@@ -116,6 +118,44 @@ const imageContents = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'
|
|
|
116
118
|
|
|
117
119
|
const imageCompressed = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="600" height="600"><image width="600" height="600" preserveAspectRatio="none" xlink:href="data:image/png;base64,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 TkSuQmCC"/><path d="M19.04 494.382C2.001 482.76-2.83 468.022 5.006 451.587c3.27-6.858 10.156-15.228 15.3-18.599 8.904-5.834 9.948-5.675 21.728 3.31 28.014 21.367 44.09 18.882 51.548-7.971 5.951-21.426 5.252-305.141-.77-313.044-2.617-3.433-11.316-6.65-19.688-7.28-14.39-1.082-15.047-1.7-16.164-15.19L55.797 78.75h326.107l2.574 11.718c2.93 13.34-1.711 18.133-17.648 18.225-19.29.11-21.83 7.13-21.83 60.325V217.5h135v-48.169c0-41.902-.892-48.976-6.858-54.375-3.772-3.413-10.384-6.206-14.693-6.206-15.006 0-19.699-4.116-19.699-17.276V78.75h161.703l-1.164 14.063c-1.132 13.677-1.605 14.094-17.28 15.224-13.45.97-16.737 2.798-19.876 11.053-5.802 15.26-2.837 249.42 3.25 256.747 2.617 3.15 11.477 6.801 19.688 8.114C599.596 386.274 600 386.745 600 401.37v15.03l-79.687-1.012-79.688-1.012-1.137-13.607c-1.135-13.594-1.12-13.61 16.875-16.305 9.907-1.484 19.252-4.642 20.767-7.018 1.515-2.376 2.781-30.054 2.813-61.508L480 258.75H344.814l1.03 60.938 1.031 60.937 19.688 3.34c20.492 3.476 22.165 5.78 17.323 23.848-2.253 8.408-2.53 8.437-80.573 8.437H225v-14.956c0-14.536.418-15.022 14.93-17.343 8.21-1.313 17.07-4.966 19.687-8.119 3.637-4.381 4.758-35.56 4.758-132.343 0-69.636-1.23-127.842-2.735-129.348-9.738-9.743-68.924-8.348-79.154 1.866-3.087 3.081-4.993 46.77-6.525 149.486-1.455 97.609-3.646 149.68-6.69 159.02-13.551 41.573-61.403 74.912-111 77.337-22.755 1.112-28.167.082-39.232-7.468z"/></svg>'
|
|
118
120
|
|
|
121
|
+
// Build a small but real PNG so the test exercises binary data (a text-only SVG hid Gulp 5's utf8 corruption of images).
|
|
122
|
+
const crc32 = buffer => {
|
|
123
|
+
let crc = 0xffffffff
|
|
124
|
+
for (const byte of buffer) {
|
|
125
|
+
crc ^= byte
|
|
126
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
127
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
131
|
+
}
|
|
132
|
+
const pngChunk = (type, data) => {
|
|
133
|
+
const length = Buffer.alloc(4)
|
|
134
|
+
length.writeUInt32BE(data.length)
|
|
135
|
+
const body = Buffer.concat([Buffer.from(type), data])
|
|
136
|
+
const crc = Buffer.alloc(4)
|
|
137
|
+
crc.writeUInt32BE(crc32(body))
|
|
138
|
+
return Buffer.concat([length, body, crc])
|
|
139
|
+
}
|
|
140
|
+
const makePng = (size = 32) => {
|
|
141
|
+
const header = Buffer.alloc(13)
|
|
142
|
+
header.writeUInt32BE(size, 0)
|
|
143
|
+
header.writeUInt32BE(size, 4)
|
|
144
|
+
header.set([8, 2, 0, 0, 0], 8) // 8-bit RGB
|
|
145
|
+
const rows = Buffer.alloc((size * 3 + 1) * size)
|
|
146
|
+
for (let y = 0; y < size; y++) {
|
|
147
|
+
for (let x = 0; x < size * 3; x++) {
|
|
148
|
+
rows[y * (size * 3 + 1) + 1 + x] = (x * 37 + y * 91) % 256 // bytes above 0x7f exercise utf8 corruption
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return Buffer.concat([
|
|
152
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
153
|
+
pngChunk('IHDR', header),
|
|
154
|
+
pngChunk('IDAT', zlib.deflateSync(rows)),
|
|
155
|
+
pngChunk('IEND', Buffer.alloc(0))
|
|
156
|
+
])
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
beforeEach(setUp.beforeEach)
|
|
120
160
|
|
|
121
161
|
afterEach(setUp.afterEach)
|
|
@@ -141,4 +181,46 @@ describe('imagesFor', () => {
|
|
|
141
181
|
done()
|
|
142
182
|
})
|
|
143
183
|
})
|
|
184
|
+
|
|
185
|
+
test('keeps binary images intact - Gulp 5 would otherwise decode them as utf8 text and corrupt or drop them', done => {
|
|
186
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
187
|
+
const pngFile = `${srcPath}/binary.png`
|
|
188
|
+
const original = makePng()
|
|
189
|
+
fs.writeFileSync(pngFile, original)
|
|
190
|
+
expect.assertions(4)
|
|
191
|
+
const browserPath = gulpConfig.get('browser.to')
|
|
192
|
+
imagesFor(pngFile, browserPath)
|
|
193
|
+
.on('finish', () => {
|
|
194
|
+
const outputPath = `${browserPath}/binary.png`
|
|
195
|
+
expect(fs.existsSync(outputPath)).toBeTruthy()
|
|
196
|
+
const optimized = fs.readFileSync(outputPath)
|
|
197
|
+
// Still a real PNG (signature and IEND intact) and never bigger than what went in.
|
|
198
|
+
expect(optimized.subarray(0, 8).equals(original.subarray(0, 8))).toBeTruthy()
|
|
199
|
+
expect(optimized.subarray(-12).equals(original.subarray(-12))).toBeTruthy()
|
|
200
|
+
expect(optimized.length).toBeLessThanOrEqual(original.length)
|
|
201
|
+
done()
|
|
202
|
+
})
|
|
203
|
+
.on('error', error => {
|
|
204
|
+
console.error('Encountered error', error)
|
|
205
|
+
done()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('loadImagemin', () => {
|
|
211
|
+
test('loads the gulp-imagemin plugin factory from the project directory', () => {
|
|
212
|
+
expect.assertions(1)
|
|
213
|
+
expect(typeof loadImagemin()).toBe('function')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('throws a helpful error with install instructions when gulp-imagemin is not installed', () => {
|
|
217
|
+
expect.assertions(2)
|
|
218
|
+
const emptyProject = fs.mkdtempSync(`${os.tmpdir()}/no-imagemin-`)
|
|
219
|
+
try {
|
|
220
|
+
expect(() => loadImagemin(emptyProject)).toThrow(/optional peer dependency gulp-imagemin, which is not installed/)
|
|
221
|
+
expect(() => loadImagemin(emptyProject)).toThrow(/github:jheagle\/gulp-imagemin#common-js-compatibility/)
|
|
222
|
+
} finally {
|
|
223
|
+
fs.rmSync(emptyProject, { recursive: true })
|
|
224
|
+
}
|
|
225
|
+
})
|
|
144
226
|
})
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { access, rm } from 'node:fs/promises'
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
|
+
|
|
4
|
+
const attempts = 4
|
|
5
|
+
|
|
6
|
+
const exists = dirPath => access(dirPath).then(() => true, () => false)
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
|
-
*
|
|
9
|
+
* Delete a directory and everything in it, resolving only once it is really gone.
|
|
10
|
+
*
|
|
11
|
+
* A single rm can leave a directory behind - most often when something is still writing into it (a build task, a
|
|
12
|
+
* watcher, a dev server) or has it briefly locked (EBUSY/EPERM/ENOTEMPTY) - which silently leaves stale output for
|
|
13
|
+
* the next build. So: Node's own retry-with-back-off handles the transient errors, then the directory is checked and
|
|
14
|
+
* removed again if it was recreated, and a clear error is raised rather than carrying on with stale output.
|
|
5
15
|
* @memberOf module:partials
|
|
6
16
|
* @param {string} dirPath
|
|
7
|
-
* @returns {Promise
|
|
17
|
+
* @returns {Promise<string>} Resolves with dirPath once the directory no longer exists (or already didn't).
|
|
18
|
+
* @throws {Error} If the directory still exists after several attempts.
|
|
8
19
|
*/
|
|
9
|
-
export const removeDirectory = dirPath =>
|
|
10
|
-
(
|
|
11
|
-
dirPath,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
export const removeDirectory = async dirPath => {
|
|
21
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
22
|
+
await rm(dirPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
23
|
+
if (!await exists(dirPath)) {
|
|
24
|
+
return dirPath
|
|
25
|
+
}
|
|
26
|
+
if (attempt < attempts) {
|
|
27
|
+
await sleep(200 * attempt)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unable to remove "${dirPath}" - it was recreated or is locked. Stop anything that has it open or is writing ` +
|
|
32
|
+
'into it (watchers, dev servers, editors, other build tasks) and try again.'
|
|
19
33
|
)
|
|
20
|
-
|
|
34
|
+
}
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
2
3
|
import { removeDirectory } from './removeDirectory.mjs'
|
|
3
4
|
|
|
5
|
+
// Wrap rm so tests can simulate something recreating the directory while (or right after) it is deleted.
|
|
6
|
+
jest.mock('node:fs/promises', () => {
|
|
7
|
+
const actual = jest.requireActual('node:fs/promises')
|
|
8
|
+
return { ...actual, rm: jest.fn(actual.rm) }
|
|
9
|
+
})
|
|
10
|
+
const { rm: realRm } = jest.requireActual('node:fs/promises')
|
|
11
|
+
|
|
12
|
+
const dirPath = 'remove-directory'
|
|
13
|
+
const recreateAfterRemoving = async (target, options) => {
|
|
14
|
+
await realRm(target, options)
|
|
15
|
+
fs.mkdirSync(target)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rm.mockReset()
|
|
20
|
+
rm.mockImplementation(realRm)
|
|
21
|
+
fs.rmSync(dirPath, { recursive: true, force: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
4
24
|
describe('removeDirectory', () => {
|
|
5
25
|
test('able to delete the remove-directory', async () => {
|
|
6
26
|
const dirPath = 'remove-directory'
|
|
@@ -25,4 +45,28 @@ describe('removeDirectory', () => {
|
|
|
25
45
|
expect(fs.existsSync(dirPath)).toBeFalsy()
|
|
26
46
|
expect(await removeDirectory(dirPath)).toBe(dirPath)
|
|
27
47
|
})
|
|
48
|
+
|
|
49
|
+
test('removes with force and Node\'s built-in retry-with-back-off for busy or locked directories', async () => {
|
|
50
|
+
expect.assertions(1)
|
|
51
|
+
fs.mkdirSync(dirPath)
|
|
52
|
+
await removeDirectory(dirPath)
|
|
53
|
+
expect(rm).toHaveBeenCalledWith(dirPath, expect.objectContaining({ recursive: true, force: true, maxRetries: 5 }))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('removes the directory again when something recreates it right after the first removal', async () => {
|
|
57
|
+
expect.assertions(2)
|
|
58
|
+
fs.mkdirSync(dirPath)
|
|
59
|
+
rm.mockImplementationOnce(recreateAfterRemoving)
|
|
60
|
+
await removeDirectory(dirPath)
|
|
61
|
+
expect(fs.existsSync(dirPath)).toBeFalsy()
|
|
62
|
+
expect(rm).toHaveBeenCalledTimes(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('fails with a clear error instead of leaving stale output when the directory keeps coming back', async () => {
|
|
66
|
+
expect.assertions(2)
|
|
67
|
+
fs.mkdirSync(dirPath)
|
|
68
|
+
rm.mockImplementation(recreateAfterRemoving)
|
|
69
|
+
await expect(removeDirectory(dirPath)).rejects.toThrow(/Unable to remove "remove-directory" - it was recreated or is locked/)
|
|
70
|
+
expect(rm).toHaveBeenCalledTimes(4)
|
|
71
|
+
})
|
|
28
72
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { dest, src } from 'gulp'
|
|
2
2
|
import * as gulpConfig from '../../gulp.config.mjs'
|
|
3
|
-
import
|
|
3
|
+
import autoprefixer from 'autoprefixer'
|
|
4
|
+
import cssnano from 'cssnano'
|
|
5
|
+
import postcss from 'gulp-postcss'
|
|
4
6
|
import rename from 'gulp-rename'
|
|
5
7
|
import * as sass from 'sass'
|
|
6
8
|
import { default as gulpSass } from 'gulp-sass'
|
|
@@ -17,6 +19,7 @@ const runSass = gulpSass(sass)
|
|
|
17
19
|
export const sassFor = (srcSearch = gulpConfig.get('sass.from'), cssPath = gulpConfig.get('sass.to')) => src(srcSearch)
|
|
18
20
|
.pipe(runSass().on('error', runSass.logError)) // Passes it through a gulp-sass, log errors to console
|
|
19
21
|
.pipe(dest(cssPath))
|
|
20
|
-
.
|
|
22
|
+
// add: false - the old gulp-cssnano only ever removed obsolete vendor prefixes, never added any; keep that.
|
|
23
|
+
.pipe(postcss([autoprefixer({ add: false }), cssnano()]))
|
|
21
24
|
.pipe(rename({ extname: '.min.css' }))
|
|
22
25
|
.pipe(dest(cssPath))
|
|
@@ -27,10 +27,7 @@ const sassContents = 'html\n' +
|
|
|
27
27
|
|
|
28
28
|
const sassPath = gulpConfig.get('sass.path')
|
|
29
29
|
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
setUp.beforeEach()
|
|
32
|
-
return fs.mkdirSync(sassPath, { recursive: true })
|
|
33
|
-
})
|
|
30
|
+
beforeEach(() => setUp.beforeEach().then(() => fs.mkdirSync(sassPath, { recursive: true })))
|
|
34
31
|
|
|
35
32
|
afterEach(setUp.afterEach)
|
|
36
33
|
|
|
@@ -60,4 +57,25 @@ describe('sassFor', () => {
|
|
|
60
57
|
done()
|
|
61
58
|
})
|
|
62
59
|
})
|
|
60
|
+
|
|
61
|
+
test('also writes a minified .min.css next to the css output', done => {
|
|
62
|
+
const sassFile = `${sassPath}/sassFor.sass`
|
|
63
|
+
fs.writeFileSync(sassFile, sassContents)
|
|
64
|
+
expect.assertions(4)
|
|
65
|
+
const cssPath = gulpConfig.get('sass.to')
|
|
66
|
+
sassFor(sassFile)
|
|
67
|
+
.on('finish', () => {
|
|
68
|
+
const cssContents = fs.readFileSync(`${cssPath}/sassFor.css`).toString()
|
|
69
|
+
const minContents = fs.readFileSync(`${cssPath}/sassFor.min.css`).toString()
|
|
70
|
+
expect(minContents.length).toBeLessThan(cssContents.length)
|
|
71
|
+
expect(countMatches(minContents.trim(), '\n')).toEqual(0)
|
|
72
|
+
expect(minContents).toContain('outline:none')
|
|
73
|
+
expect(minContents).toContain('text-align:center')
|
|
74
|
+
done()
|
|
75
|
+
})
|
|
76
|
+
.on('error', error => {
|
|
77
|
+
console.error('Encountered error', error)
|
|
78
|
+
done()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
63
81
|
})
|