js-build-tools 6.7.0 → 6.8.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 +6 -0
- package/dist/partials/cacheFiles.js +121 -0
- package/dist/partials/imagesFor.js +75 -13
- package/dist/partials/jsdocDocsFor.js +18 -5
- package/dist/partials.js +7 -0
- package/dist/partials.min.js +1 -1
- package/dist/test-helpers/setUp.js +2 -0
- package/functions/build.test.mjs +2 -5
- package/functions/images.test.mjs +2 -4
- package/functions/partials/cacheFiles.mjs +114 -0
- package/functions/partials/cacheFiles.test.mjs +151 -0
- package/functions/partials/imagesFor.mjs +75 -11
- package/functions/partials/imagesFor.test.mjs +29 -5
- package/functions/partials.mjs +1 -0
- package/functions/partials.test.mjs +1 -1
- package/functions/test-helpers/setUp.mjs +2 -0
- package/gulp.config.mjs +5 -0
- package/package.json +3 -3
- package/dist/test-helpers/isolateGulpCache.js +0 -25
- package/functions/test-helpers/isolateGulpCache.mjs +0 -16
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ provided are the defaults):
|
|
|
86
86
|
"to": "browser/fonts"
|
|
87
87
|
},
|
|
88
88
|
"images": {
|
|
89
|
+
// Where optimized images are cached between runs (an unchanged image isn't optimized again)
|
|
90
|
+
"cacheDir": "node_modules/.cache/js-build-tools/images",
|
|
89
91
|
// Toggle image minify and copy process
|
|
90
92
|
"enabled": false,
|
|
91
93
|
// Path to search for images
|
|
@@ -437,6 +439,9 @@ Add the following to the exports in your `build-tools.config.json`:
|
|
|
437
439
|
```json
|
|
438
440
|
{
|
|
439
441
|
"images": {
|
|
442
|
+
// Where optimized images are cached between runs, so an image that hasn't changed isn't optimized again. Inside
|
|
443
|
+
// node_modules by default, so it's never committed; delete the folder to start over
|
|
444
|
+
"cacheDir": "node_modules/.cache/js-build-tools/images",
|
|
440
445
|
// Enable image process
|
|
441
446
|
"enabled": false,
|
|
442
447
|
// Search pattern to find your images
|
|
@@ -1114,6 +1119,7 @@ Configurations to minify and copy the images.
|
|
|
1114
1119
|
|
|
1115
1120
|
| Name | Type | Description |
|
|
1116
1121
|
| --- | --- | --- |
|
|
1122
|
+
| cacheDir | <code>StringSetting</code> | Where optimized images are cached between runs, so an image that hasn't changed isn't optimized again. An upgrade of imagemin (or its plugins) or other settings starts new results. Inside node_modules by default, so it's never committed; old entries aren't pruned, so delete it now and then to start over. |
|
|
1117
1123
|
| enabled | <code>BooleanSetting</code> | Toggle image minify and copy process. |
|
|
1118
1124
|
| from | <code>StringSetting</code> | Path to search for images. |
|
|
1119
1125
|
| jpegQuality | <code>FlagNumberSetting</code> | The JPEG quality (0 to 100) mozjpeg compresses to. False (the default) keeps mozjpeg's own default, 75. Raise it (85 is usually indistinguishable from the original) when 75 visibly bands smooth gradients. |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.cacheFiles = void 0;
|
|
7
|
+
var _crypto = require("crypto");
|
|
8
|
+
var _promises = require("fs/promises");
|
|
9
|
+
var _path = _interopRequireDefault(require("path"));
|
|
10
|
+
var _stream = require("stream");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
// Marks a file sent to the plugin with its cache file and its path, so its result is cached even if the plugin renames it
|
|
13
|
+
const cacheFile = Symbol('cacheFile');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The cache file for a file's contents: a hash of the cache name, the fingerprint and the contents, so the same file
|
|
17
|
+
* with other plugin settings or plugin versions gets a result of its own.
|
|
18
|
+
* @private
|
|
19
|
+
* @param {string} directory
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @param {string} fingerprint
|
|
22
|
+
* @param {Buffer} contents
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
const cachePath = (directory, name, fingerprint, contents) => _path.default.join(directory, name, (0, _crypto.createHash)('sha256').update(name).update('\0').update(fingerprint).update('\0').update(contents).digest('hex'));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write through a temporary file and rename it, so a reader (another build or test worker) never sees a half-written
|
|
29
|
+
* result.
|
|
30
|
+
* @private
|
|
31
|
+
* @param {string} file
|
|
32
|
+
* @param {Buffer} contents
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
const writeAtomic = async (file, contents) => {
|
|
36
|
+
await (0, _promises.mkdir)(_path.default.dirname(file), {
|
|
37
|
+
recursive: true
|
|
38
|
+
});
|
|
39
|
+
const temporary = `${file}.${(0, _crypto.randomUUID)()}.tmp`;
|
|
40
|
+
await (0, _promises.writeFile)(temporary, contents);
|
|
41
|
+
await (0, _promises.rename)(temporary, file);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run a gulp plugin only on files it hasn't processed before. A file whose contents (with this cache name and
|
|
46
|
+
* fingerprint) were processed by an earlier run gets that run's result from the cache instead, and skips the plugin.
|
|
47
|
+
* Replaces gulp-cache, which kept one cache for every process in the system's temp folder.
|
|
48
|
+
*
|
|
49
|
+
* A result is cached when the plugin passes on the file object it was given (as gulp plugins usually do); a plugin
|
|
50
|
+
* that makes new file objects still works, its results just aren't cached. When the plugin renames a file, the cache
|
|
51
|
+
* keeps the new name too (next to the result, as `<entry>.path`), so a cached file is renamed the same way. Entries
|
|
52
|
+
* are never pruned: delete the directory to start over.
|
|
53
|
+
* @memberOf module:partials
|
|
54
|
+
* @param {stream.Transform} plugin - The gulp plugin stream to run on uncached files (e.g. imagemin()).
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
* @param {string} options.directory - Where cached results are kept (see images.cacheDir).
|
|
57
|
+
* @param {string} [options.name='default'] - The cache's folder; keeps results of different plugin settings apart.
|
|
58
|
+
* @param {string} [options.fingerprint=''] - Anything else that changes the plugin's output, such as its version and
|
|
59
|
+
* options: when it changes, earlier results are no longer used.
|
|
60
|
+
* @returns {stream.Transform}
|
|
61
|
+
*/
|
|
62
|
+
const cacheFiles = (plugin, {
|
|
63
|
+
directory,
|
|
64
|
+
name = 'default',
|
|
65
|
+
fingerprint = ''
|
|
66
|
+
}) => {
|
|
67
|
+
const writes = [];
|
|
68
|
+
const pluginDone = new Promise((resolve, reject) => {
|
|
69
|
+
plugin.on('end', resolve);
|
|
70
|
+
plugin.on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
// The error also reaches the stream (below); this only stops it from being reported a second time as unhandled
|
|
73
|
+
pluginDone.catch(() => {});
|
|
74
|
+
const stream = new _stream.Transform({
|
|
75
|
+
objectMode: true,
|
|
76
|
+
transform(file, encoding, callback) {
|
|
77
|
+
if (!file.isBuffer()) {
|
|
78
|
+
callback(null, file);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const cached = cachePath(directory, name, fingerprint, file.contents);
|
|
82
|
+
(0, _promises.readFile)(cached).then(async contents => {
|
|
83
|
+
file.contents = contents;
|
|
84
|
+
// Written before the result, so it's there whenever the result is
|
|
85
|
+
const renamed = await (0, _promises.readFile)(`${cached}.path`, 'utf8').catch(() => undefined);
|
|
86
|
+
if (renamed !== undefined) {
|
|
87
|
+
file.path = _path.default.join(_path.default.dirname(file.path), renamed);
|
|
88
|
+
}
|
|
89
|
+
callback(null, file);
|
|
90
|
+
}, () => {
|
|
91
|
+
file[cacheFile] = {
|
|
92
|
+
cached,
|
|
93
|
+
from: file.path
|
|
94
|
+
};
|
|
95
|
+
plugin.write(file);
|
|
96
|
+
callback();
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
flush(callback) {
|
|
100
|
+
plugin.end();
|
|
101
|
+
pluginDone.then(() => Promise.all(writes)).then(() => callback(), callback);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
plugin.on('data', file => {
|
|
105
|
+
const tag = file[cacheFile];
|
|
106
|
+
delete file[cacheFile];
|
|
107
|
+
if (tag && file.isBuffer()) {
|
|
108
|
+
const {
|
|
109
|
+
cached,
|
|
110
|
+
from
|
|
111
|
+
} = tag;
|
|
112
|
+
const renamed = file.path === from ? Promise.resolve() : writeAtomic(`${cached}.path`, _path.default.relative(_path.default.dirname(from), file.path));
|
|
113
|
+
// The cache only saves time: a result that can't be written is processed again next run, not a failed build
|
|
114
|
+
writes.push(renamed.then(() => writeAtomic(cached, file.contents)).catch(() => {}));
|
|
115
|
+
}
|
|
116
|
+
stream.push(file);
|
|
117
|
+
});
|
|
118
|
+
plugin.on('error', error => stream.destroy(error));
|
|
119
|
+
return stream;
|
|
120
|
+
};
|
|
121
|
+
exports.cacheFiles = cacheFiles;
|
|
@@ -4,8 +4,11 @@ require("core-js/modules/esnext.weak-map.delete-all.js");
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.loadImageminPlugins = exports.loadImagemin = exports.imagesFor = exports.imageminPlugins = void 0;
|
|
8
|
-
|
|
7
|
+
exports.loadImageminPlugins = exports.loadImagemin = exports.imagesFor = exports.imageminVersions = exports.imageminPlugins = void 0;
|
|
8
|
+
require("core-js/modules/es.json.stringify.js");
|
|
9
|
+
var _fs = require("fs");
|
|
10
|
+
var _path = _interopRequireDefault(require("path"));
|
|
11
|
+
var _cacheFiles = require("./cacheFiles.js");
|
|
9
12
|
var _loadPeer = require("./loadPeer.js");
|
|
10
13
|
var _gulp = require("gulp");
|
|
11
14
|
var gulpConfig = _interopRequireWildcard(require("../../gulp.config.js"));
|
|
@@ -63,6 +66,59 @@ const imageminPlugins = (jpegQuality, plugins = loadImageminPlugins()) => defaul
|
|
|
63
66
|
quality: jpegQuality
|
|
64
67
|
}) : makePlugin()));
|
|
65
68
|
|
|
69
|
+
// The packages that decide imagemin's output: the wrapper and imagemin, then each plugin with the engine it runs
|
|
70
|
+
exports.imageminPlugins = imageminPlugins;
|
|
71
|
+
const imageminPackages = [['gulp-imagemin'], ['imagemin'], ['imagemin-gifsicle', 'gifsicle'], ['imagemin-mozjpeg', 'mozjpeg'], ['imagemin-optipng', 'optipng-bin'], ['imagemin-svgo', 'svgo']];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Find an installed package the way Node does, from a directory up through each node_modules folder. Reads the
|
|
75
|
+
* package.json directly, so it also works for ESM-only packages that don't export it.
|
|
76
|
+
* @private
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @param {string} fromDirectory
|
|
79
|
+
* @returns {{directory: string, version: string}|undefined}
|
|
80
|
+
*/
|
|
81
|
+
const findPackage = (name, fromDirectory) => {
|
|
82
|
+
for (let directory = _path.default.resolve(fromDirectory);; directory = _path.default.dirname(directory)) {
|
|
83
|
+
const file = _path.default.join(directory, 'node_modules', name, 'package.json');
|
|
84
|
+
if ((0, _fs.existsSync)(file)) {
|
|
85
|
+
return {
|
|
86
|
+
directory: _path.default.dirname(file),
|
|
87
|
+
version: JSON.parse((0, _fs.readFileSync)(file, 'utf8')).version
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (directory === _path.default.dirname(directory)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The installed versions of the packages that decide imagemin's output, so the image cache stops using results made
|
|
98
|
+
* by older versions after an upgrade (see images.cacheDir).
|
|
99
|
+
* @memberOf module:partials
|
|
100
|
+
* @param {string} [projectPath=process.cwd()] - The project directory gulp-imagemin is installed in.
|
|
101
|
+
* @returns {Object<string, string>} Each package's version by name; a package that isn't installed is left out.
|
|
102
|
+
*/
|
|
103
|
+
const imageminVersions = (projectPath = process.cwd()) => {
|
|
104
|
+
const gulpImagemin = findPackage('gulp-imagemin', projectPath);
|
|
105
|
+
const versions = {};
|
|
106
|
+
if (!gulpImagemin) {
|
|
107
|
+
return versions;
|
|
108
|
+
}
|
|
109
|
+
for (const [wrapper, engine] of imageminPackages) {
|
|
110
|
+
const found = findPackage(wrapper, gulpImagemin.directory);
|
|
111
|
+
if (found) {
|
|
112
|
+
versions[wrapper] = found.version;
|
|
113
|
+
const engineFound = engine && findPackage(engine, found.directory);
|
|
114
|
+
if (engineFound) {
|
|
115
|
+
versions[engine] = engineFound.version;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return versions;
|
|
120
|
+
};
|
|
121
|
+
|
|
66
122
|
/**
|
|
67
123
|
* Move and optimize images into the browser directory.
|
|
68
124
|
* @memberOf module:partials
|
|
@@ -70,25 +126,31 @@ const imageminPlugins = (jpegQuality, plugins = loadImageminPlugins()) => defaul
|
|
|
70
126
|
* @param {string} [imageDest=dest/image/folder]
|
|
71
127
|
* @return {stream.Stream}
|
|
72
128
|
*/
|
|
73
|
-
exports.
|
|
129
|
+
exports.imageminVersions = imageminVersions;
|
|
74
130
|
const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest = gulpConfig.get('images.to')) => {
|
|
75
131
|
const imagemin = loadImagemin();
|
|
76
132
|
const jpegQuality = gulpConfig.get('images.jpegQuality');
|
|
77
133
|
// Left unset, imagemin runs its default plugins with their own defaults
|
|
78
134
|
const plugins = jpegQuality ? imageminPlugins(jpegQuality) : undefined;
|
|
79
|
-
|
|
80
|
-
return (0, _gulp.src)(imageSrc, {
|
|
81
|
-
encoding: false
|
|
82
|
-
}).pipe(
|
|
83
|
-
// Caching images that ran through imagemin
|
|
84
|
-
(0, _gulpCache.default)(imagemin(plugins, {
|
|
135
|
+
const options = {
|
|
85
136
|
interlaced: true,
|
|
86
137
|
silent: true,
|
|
87
138
|
verbose: false
|
|
88
|
-
}
|
|
89
|
-
//
|
|
90
|
-
{
|
|
91
|
-
|
|
139
|
+
};
|
|
140
|
+
// Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images.
|
|
141
|
+
return (0, _gulp.src)(imageSrc, {
|
|
142
|
+
encoding: false
|
|
143
|
+
}).pipe(
|
|
144
|
+
// Only images imagemin hasn't optimized before go through it; the rest come from the cache
|
|
145
|
+
(0, _cacheFiles.cacheFiles)(imagemin(plugins, options), {
|
|
146
|
+
directory: gulpConfig.get('images.cacheDir'),
|
|
147
|
+
// Each quality gets results of its own
|
|
148
|
+
name: jpegQuality ? `jpeg-quality-${jpegQuality}` : 'default',
|
|
149
|
+
// A new imagemin (or plugin, or engine) version, or other options, makes new results
|
|
150
|
+
fingerprint: JSON.stringify({
|
|
151
|
+
options,
|
|
152
|
+
versions: imageminVersions()
|
|
153
|
+
})
|
|
92
154
|
})).pipe((0, _gulp.dest)(imageDest));
|
|
93
155
|
};
|
|
94
156
|
exports.imagesFor = imagesFor;
|
|
@@ -122,7 +122,15 @@ const jsdocDocsFor = (srcPath = gulpConfig.get('docs.from'), outPath = gulpConfi
|
|
|
122
122
|
const themeDir = resolveJsdocTheme(theme);
|
|
123
123
|
const indexFile = gulpConfig.get('docs.index');
|
|
124
124
|
const title = gulpConfig.get('docs.title');
|
|
125
|
+
const basePath = gulpConfig.get('docs.basePath');
|
|
125
126
|
await (0, _removeDirectory.removeDirectory)(outPath);
|
|
127
|
+
const workDir = _nodeFs.default.mkdtempSync(_nodePath.default.join(_nodeOs.default.tmpdir(), 'jsdocdocs-'));
|
|
128
|
+
// clean-jsdoc-theme 5.x writes no index.html at all without a front page, so fall back to one with the title
|
|
129
|
+
let readme = indexFile && _nodeFs.default.existsSync(indexFile) ? _nodePath.default.resolve(indexFile) : '';
|
|
130
|
+
if (!readme) {
|
|
131
|
+
readme = _nodePath.default.join(workDir, 'index.md');
|
|
132
|
+
_nodeFs.default.writeFileSync(readme, `# ${title || 'Documentation'}\n`);
|
|
133
|
+
}
|
|
126
134
|
const config = {
|
|
127
135
|
..._jsdocBase.default,
|
|
128
136
|
source: {
|
|
@@ -136,21 +144,26 @@ const jsdocDocsFor = (srcPath = gulpConfig.get('docs.from'), outPath = gulpConfi
|
|
|
136
144
|
// quick-start example, which sets this explicitly, not assumed from jsdoc's own classic defaults.
|
|
137
145
|
recurse: true,
|
|
138
146
|
template: themeDir,
|
|
139
|
-
// clean-jsdoc-theme's own options for the name in its header and the home page's <title> (otherwise "Home").
|
|
147
|
+
// clean-jsdoc-theme 4.x's own options for the name in its header and the home page's <title> (otherwise "Home").
|
|
140
148
|
// The header is given as a link: a plain-text title links to the site root ("/"), while every page is in one
|
|
141
149
|
// folder, so index.html is the home page wherever they are served. Both are printed unescaped by the theme.
|
|
150
|
+
// clean-jsdoc-theme 5.x reads its options from opts itself: the name in its header and <title>, and the path
|
|
151
|
+
// its root-absolute links start from (4.x reads only theme_opts).
|
|
152
|
+
...(title ? {
|
|
153
|
+
siteName: title
|
|
154
|
+
} : {}),
|
|
155
|
+
...(basePath ? {
|
|
156
|
+
basePath
|
|
157
|
+
} : {}),
|
|
142
158
|
...(title ? {
|
|
143
159
|
theme_opts: {
|
|
144
160
|
title: `<a href="index.html" class="sidebar-title sidebar-title-anchor">${escapeHtml(title)}</a>`,
|
|
145
161
|
homepageTitle: escapeHtml(title)
|
|
146
162
|
}
|
|
147
163
|
} : {}),
|
|
148
|
-
|
|
149
|
-
readme: _nodePath.default.resolve(indexFile)
|
|
150
|
-
} : {})
|
|
164
|
+
readme
|
|
151
165
|
}
|
|
152
166
|
};
|
|
153
|
-
const workDir = _nodeFs.default.mkdtempSync(_nodePath.default.join(_nodeOs.default.tmpdir(), 'jsdocdocs-'));
|
|
154
167
|
try {
|
|
155
168
|
const configPath = _nodePath.default.join(workDir, 'jsdoc.conf.json');
|
|
156
169
|
_nodeFs.default.writeFileSync(configPath, JSON.stringify(config));
|
package/dist/partials.js
CHANGED
|
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "beginWatcher", {
|
|
|
15
15
|
return _beginWatcher.beginWatcher;
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
|
+
Object.defineProperty(exports, "cacheFiles", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _cacheFiles.cacheFiles;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
18
24
|
Object.defineProperty(exports, "clean", {
|
|
19
25
|
enumerable: true,
|
|
20
26
|
get: function () {
|
|
@@ -155,6 +161,7 @@ Object.defineProperty(exports, "typeDocsFor", {
|
|
|
155
161
|
});
|
|
156
162
|
var _addToReadme = require("./partials/addToReadme.js");
|
|
157
163
|
var _beginWatcher = require("./partials/beginWatcher.js");
|
|
164
|
+
var _cacheFiles = require("./partials/cacheFiles.js");
|
|
158
165
|
var _clean = require("./partials/clean.js");
|
|
159
166
|
var _copyFor = require("./partials/copyFor.js");
|
|
160
167
|
var _distFor = require("./partials/distFor.js");
|
package/dist/partials.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"addToReadme",{enumerable:!0,get:function(){return _addToReadme.addToReadme}}),Object.defineProperty(exports,"beginWatcher",{enumerable:!0,get:function(){return _beginWatcher.beginWatcher}}),Object.defineProperty(exports,"clean",{enumerable:!0,get:function(){return _clean.clean}}),Object.defineProperty(exports,"copyFor",{enumerable:!0,get:function(){return _copyFor.copyFor}}),Object.defineProperty(exports,"distFor",{enumerable:!0,get:function(){return _distFor.distFor}}),Object.defineProperty(exports,"distForSrc",{enumerable:!0,get:function(){return _distForSrc.distForSrc}}),Object.defineProperty(exports,"distSeries",{enumerable:!0,get:function(){return _distSeries.distSeries}}),Object.defineProperty(exports,"imagesFor",{enumerable:!0,get:function(){return _imagesFor.imagesFor}}),Object.defineProperty(exports,"importModule",{enumerable:!0,get:function(){return _loadPeer.importModule}}),Object.defineProperty(exports,"importPeer",{enumerable:!0,get:function(){return _loadPeer.importPeer}}),Object.defineProperty(exports,"jsdocDocsFor",{enumerable:!0,get:function(){return _jsdocDocsFor.jsdocDocsFor}}),Object.defineProperty(exports,"launchBrowser",{enumerable:!0,get:function(){return _loadPlaywright.launchBrowser}}),Object.defineProperty(exports,"lintSource",{enumerable:!0,get:function(){return _lintSource.lintSource}}),Object.defineProperty(exports,"loadPlaywright",{enumerable:!0,get:function(){return _loadPlaywright.loadPlaywright}}),Object.defineProperty(exports,"minifyFor",{enumerable:!0,get:function(){return _minifyFor.minifyFor}}),Object.defineProperty(exports,"neostandardLint",{enumerable:!0,get:function(){return _neostandardLint.neostandardLint}}),Object.defineProperty(exports,"readmeTemplate",{enumerable:!0,get:function(){return _readmeTemplate.readmeTemplate}}),Object.defineProperty(exports,"removeDirectory",{enumerable:!0,get:function(){return _removeDirectory.removeDirectory}}),Object.defineProperty(exports,"requirePeer",{enumerable:!0,get:function(){return _loadPeer.requirePeer}}),Object.defineProperty(exports,"resolveDocsMethod",{enumerable:!0,get:function(){return _resolveDocsMethod.resolveDocsMethod}}),Object.defineProperty(exports,"runOnChange",{enumerable:!0,get:function(){return _runOnChange.runOnChange}}),Object.defineProperty(exports,"sassFor",{enumerable:!0,get:function(){return _sassFor.sassFor}}),Object.defineProperty(exports,"serveStatic",{enumerable:!0,get:function(){return _serveStatic.serveStatic}}),Object.defineProperty(exports,"tsFor",{enumerable:!0,get:function(){return _tsFor.tsFor}}),Object.defineProperty(exports,"typeDocsFor",{enumerable:!0,get:function(){return _typeDocsFor.typeDocsFor}});var _addToReadme=require("./partials/addToReadme.js"),_beginWatcher=require("./partials/beginWatcher.js"),_clean=require("./partials/clean.js"),_copyFor=require("./partials/copyFor.js"),_distFor=require("./partials/distFor.js"),_distForSrc=require("./partials/distForSrc.js"),_distSeries=require("./partials/distSeries.js"),_imagesFor=require("./partials/imagesFor.js"),_loadPeer=require("./partials/loadPeer.js"),_jsdocDocsFor=require("./partials/jsdocDocsFor.js"),_loadPlaywright=require("./partials/loadPlaywright.js"),_lintSource=require("./partials/lintSource.js"),_minifyFor=require("./partials/minifyFor.js"),_neostandardLint=require("./partials/neostandardLint.js"),_readmeTemplate=require("./partials/readmeTemplate.js"),_removeDirectory=require("./partials/removeDirectory.js"),_resolveDocsMethod=require("./partials/resolveDocsMethod.js"),_runOnChange=require("./partials/runOnChange.js"),_sassFor=require("./partials/sassFor.js"),_serveStatic=require("./partials/serveStatic.js"),_tsFor=require("./partials/tsFor.js"),_typeDocsFor=require("./partials/typeDocsFor.js");
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"addToReadme",{enumerable:!0,get:function(){return _addToReadme.addToReadme}}),Object.defineProperty(exports,"beginWatcher",{enumerable:!0,get:function(){return _beginWatcher.beginWatcher}}),Object.defineProperty(exports,"cacheFiles",{enumerable:!0,get:function(){return _cacheFiles.cacheFiles}}),Object.defineProperty(exports,"clean",{enumerable:!0,get:function(){return _clean.clean}}),Object.defineProperty(exports,"copyFor",{enumerable:!0,get:function(){return _copyFor.copyFor}}),Object.defineProperty(exports,"distFor",{enumerable:!0,get:function(){return _distFor.distFor}}),Object.defineProperty(exports,"distForSrc",{enumerable:!0,get:function(){return _distForSrc.distForSrc}}),Object.defineProperty(exports,"distSeries",{enumerable:!0,get:function(){return _distSeries.distSeries}}),Object.defineProperty(exports,"imagesFor",{enumerable:!0,get:function(){return _imagesFor.imagesFor}}),Object.defineProperty(exports,"importModule",{enumerable:!0,get:function(){return _loadPeer.importModule}}),Object.defineProperty(exports,"importPeer",{enumerable:!0,get:function(){return _loadPeer.importPeer}}),Object.defineProperty(exports,"jsdocDocsFor",{enumerable:!0,get:function(){return _jsdocDocsFor.jsdocDocsFor}}),Object.defineProperty(exports,"launchBrowser",{enumerable:!0,get:function(){return _loadPlaywright.launchBrowser}}),Object.defineProperty(exports,"lintSource",{enumerable:!0,get:function(){return _lintSource.lintSource}}),Object.defineProperty(exports,"loadPlaywright",{enumerable:!0,get:function(){return _loadPlaywright.loadPlaywright}}),Object.defineProperty(exports,"minifyFor",{enumerable:!0,get:function(){return _minifyFor.minifyFor}}),Object.defineProperty(exports,"neostandardLint",{enumerable:!0,get:function(){return _neostandardLint.neostandardLint}}),Object.defineProperty(exports,"readmeTemplate",{enumerable:!0,get:function(){return _readmeTemplate.readmeTemplate}}),Object.defineProperty(exports,"removeDirectory",{enumerable:!0,get:function(){return _removeDirectory.removeDirectory}}),Object.defineProperty(exports,"requirePeer",{enumerable:!0,get:function(){return _loadPeer.requirePeer}}),Object.defineProperty(exports,"resolveDocsMethod",{enumerable:!0,get:function(){return _resolveDocsMethod.resolveDocsMethod}}),Object.defineProperty(exports,"runOnChange",{enumerable:!0,get:function(){return _runOnChange.runOnChange}}),Object.defineProperty(exports,"sassFor",{enumerable:!0,get:function(){return _sassFor.sassFor}}),Object.defineProperty(exports,"serveStatic",{enumerable:!0,get:function(){return _serveStatic.serveStatic}}),Object.defineProperty(exports,"tsFor",{enumerable:!0,get:function(){return _tsFor.tsFor}}),Object.defineProperty(exports,"typeDocsFor",{enumerable:!0,get:function(){return _typeDocsFor.typeDocsFor}});var _addToReadme=require("./partials/addToReadme.js"),_beginWatcher=require("./partials/beginWatcher.js"),_cacheFiles=require("./partials/cacheFiles.js"),_clean=require("./partials/clean.js"),_copyFor=require("./partials/copyFor.js"),_distFor=require("./partials/distFor.js"),_distForSrc=require("./partials/distForSrc.js"),_distSeries=require("./partials/distSeries.js"),_imagesFor=require("./partials/imagesFor.js"),_loadPeer=require("./partials/loadPeer.js"),_jsdocDocsFor=require("./partials/jsdocDocsFor.js"),_loadPlaywright=require("./partials/loadPlaywright.js"),_lintSource=require("./partials/lintSource.js"),_minifyFor=require("./partials/minifyFor.js"),_neostandardLint=require("./partials/neostandardLint.js"),_readmeTemplate=require("./partials/readmeTemplate.js"),_removeDirectory=require("./partials/removeDirectory.js"),_resolveDocsMethod=require("./partials/resolveDocsMethod.js"),_runOnChange=require("./partials/runOnChange.js"),_sassFor=require("./partials/sassFor.js"),_serveStatic=require("./partials/serveStatic.js"),_tsFor=require("./partials/tsFor.js"),_typeDocsFor=require("./partials/typeDocsFor.js");
|
|
@@ -84,6 +84,8 @@ const setDefaults = (testDir = 'test-temp') => {
|
|
|
84
84
|
gulpConfig.set('docs.title', '');
|
|
85
85
|
gulpConfig.set('fonts.from', `${srcPath}/fonts/**/*`);
|
|
86
86
|
gulpConfig.set('fonts.to', `${browserPath}/fonts`);
|
|
87
|
+
// Each test file's own cache, inside its temp directory, so beforeEach's fresh directory is also an empty cache
|
|
88
|
+
gulpConfig.set('images.cacheDir', `${tempDir}image-cache`);
|
|
87
89
|
gulpConfig.set('images.from', `${srcPath}/img/**/*.+(png|jpg|jpeg|gif|svg)`);
|
|
88
90
|
gulpConfig.set('images.to', `${browserPath}/img`);
|
|
89
91
|
gulpConfig.set('readme.template', `${tempDir}MAIN.md`);
|
package/functions/build.test.mjs
CHANGED
|
@@ -8,10 +8,8 @@ import { testFull } from './testFull.mjs'
|
|
|
8
8
|
import { typeDocs } from './typeDocs.mjs'
|
|
9
9
|
import { jsdocDocs } from './jsdocDocs.mjs'
|
|
10
10
|
import { countMatches } from 'test-filesystem'
|
|
11
|
-
import { isolateGulpCache } from './test-helpers/isolateGulpCache.mjs'
|
|
12
11
|
|
|
13
12
|
setUp.setDefaults('test-build')
|
|
14
|
-
const clearImageCache = isolateGulpCache('build')
|
|
15
13
|
const gulpConfig = setUp.gulpConfig
|
|
16
14
|
|
|
17
15
|
const genericFunction = () => Promise.resolve(true)
|
|
@@ -255,10 +253,9 @@ const sassContents = 'html\n' +
|
|
|
255
253
|
|
|
256
254
|
const sassPath = gulpConfig.get('sass.path')
|
|
257
255
|
|
|
258
|
-
//
|
|
256
|
+
// A fresh temp directory each test, which is also an empty image cache (see setUp's images.cacheDir)
|
|
259
257
|
beforeEach(
|
|
260
|
-
() =>
|
|
261
|
-
.then(setUp.beforeEach)
|
|
258
|
+
() => setUp.beforeEach()
|
|
262
259
|
.then(
|
|
263
260
|
async () => {
|
|
264
261
|
const srcPath = gulpConfig.get('srcPath')
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import * as setUp from './test-helpers/setUp.mjs'
|
|
3
3
|
import { images } from './images.mjs'
|
|
4
|
-
import { isolateGulpCache } from './test-helpers/isolateGulpCache.mjs'
|
|
5
4
|
|
|
6
5
|
setUp.setDefaults('test-images')
|
|
7
|
-
const clearImageCache = isolateGulpCache('images')
|
|
8
6
|
const gulpConfig = setUp.gulpConfig
|
|
9
7
|
|
|
10
8
|
const imageContents = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
|
|
@@ -116,8 +114,8 @@ const imageContents = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'
|
|
|
116
114
|
' inkscape:connector-curvature="0" />\n' +
|
|
117
115
|
'</svg>\n'
|
|
118
116
|
|
|
119
|
-
//
|
|
120
|
-
beforeEach(() =>
|
|
117
|
+
// A fresh temp directory each test, which is also an empty image cache (see setUp's images.cacheDir)
|
|
118
|
+
beforeEach(() => setUp.beforeEach())
|
|
121
119
|
|
|
122
120
|
afterEach(setUp.afterEach)
|
|
123
121
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto'
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { Transform } from 'stream'
|
|
5
|
+
|
|
6
|
+
// Marks a file sent to the plugin with its cache file and its path, so its result is cached even if the plugin renames it
|
|
7
|
+
const cacheFile = Symbol('cacheFile')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The cache file for a file's contents: a hash of the cache name, the fingerprint and the contents, so the same file
|
|
11
|
+
* with other plugin settings or plugin versions gets a result of its own.
|
|
12
|
+
* @private
|
|
13
|
+
* @param {string} directory
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @param {string} fingerprint
|
|
16
|
+
* @param {Buffer} contents
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
const cachePath = (directory, name, fingerprint, contents) => path.join(
|
|
20
|
+
directory,
|
|
21
|
+
name,
|
|
22
|
+
createHash('sha256').update(name).update('\0').update(fingerprint).update('\0').update(contents).digest('hex')
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Write through a temporary file and rename it, so a reader (another build or test worker) never sees a half-written
|
|
27
|
+
* result.
|
|
28
|
+
* @private
|
|
29
|
+
* @param {string} file
|
|
30
|
+
* @param {Buffer} contents
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
const writeAtomic = async (file, contents) => {
|
|
34
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
35
|
+
const temporary = `${file}.${randomUUID()}.tmp`
|
|
36
|
+
await writeFile(temporary, contents)
|
|
37
|
+
await rename(temporary, file)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a gulp plugin only on files it hasn't processed before. A file whose contents (with this cache name and
|
|
42
|
+
* fingerprint) were processed by an earlier run gets that run's result from the cache instead, and skips the plugin.
|
|
43
|
+
* Replaces gulp-cache, which kept one cache for every process in the system's temp folder.
|
|
44
|
+
*
|
|
45
|
+
* A result is cached when the plugin passes on the file object it was given (as gulp plugins usually do); a plugin
|
|
46
|
+
* that makes new file objects still works, its results just aren't cached. When the plugin renames a file, the cache
|
|
47
|
+
* keeps the new name too (next to the result, as `<entry>.path`), so a cached file is renamed the same way. Entries
|
|
48
|
+
* are never pruned: delete the directory to start over.
|
|
49
|
+
* @memberOf module:partials
|
|
50
|
+
* @param {stream.Transform} plugin - The gulp plugin stream to run on uncached files (e.g. imagemin()).
|
|
51
|
+
* @param {Object} options
|
|
52
|
+
* @param {string} options.directory - Where cached results are kept (see images.cacheDir).
|
|
53
|
+
* @param {string} [options.name='default'] - The cache's folder; keeps results of different plugin settings apart.
|
|
54
|
+
* @param {string} [options.fingerprint=''] - Anything else that changes the plugin's output, such as its version and
|
|
55
|
+
* options: when it changes, earlier results are no longer used.
|
|
56
|
+
* @returns {stream.Transform}
|
|
57
|
+
*/
|
|
58
|
+
export const cacheFiles = (plugin, { directory, name = 'default', fingerprint = '' }) => {
|
|
59
|
+
const writes = []
|
|
60
|
+
const pluginDone = new Promise((resolve, reject) => {
|
|
61
|
+
plugin.on('end', resolve)
|
|
62
|
+
plugin.on('error', reject)
|
|
63
|
+
})
|
|
64
|
+
// The error also reaches the stream (below); this only stops it from being reported a second time as unhandled
|
|
65
|
+
pluginDone.catch(() => {})
|
|
66
|
+
|
|
67
|
+
const stream = new Transform({
|
|
68
|
+
objectMode: true,
|
|
69
|
+
transform (file, encoding, callback) {
|
|
70
|
+
if (!file.isBuffer()) {
|
|
71
|
+
callback(null, file)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const cached = cachePath(directory, name, fingerprint, file.contents)
|
|
75
|
+
readFile(cached).then(
|
|
76
|
+
async contents => {
|
|
77
|
+
file.contents = contents
|
|
78
|
+
// Written before the result, so it's there whenever the result is
|
|
79
|
+
const renamed = await readFile(`${cached}.path`, 'utf8').catch(() => undefined)
|
|
80
|
+
if (renamed !== undefined) {
|
|
81
|
+
file.path = path.join(path.dirname(file.path), renamed)
|
|
82
|
+
}
|
|
83
|
+
callback(null, file)
|
|
84
|
+
},
|
|
85
|
+
() => {
|
|
86
|
+
file[cacheFile] = { cached, from: file.path }
|
|
87
|
+
plugin.write(file)
|
|
88
|
+
callback()
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
flush (callback) {
|
|
93
|
+
plugin.end()
|
|
94
|
+
pluginDone
|
|
95
|
+
.then(() => Promise.all(writes))
|
|
96
|
+
.then(() => callback(), callback)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
plugin.on('data', file => {
|
|
101
|
+
const tag = file[cacheFile]
|
|
102
|
+
delete file[cacheFile]
|
|
103
|
+
if (tag && file.isBuffer()) {
|
|
104
|
+
const { cached, from } = tag
|
|
105
|
+
const renamed = file.path === from ? Promise.resolve() : writeAtomic(`${cached}.path`, path.relative(path.dirname(from), file.path))
|
|
106
|
+
// The cache only saves time: a result that can't be written is processed again next run, not a failed build
|
|
107
|
+
writes.push(renamed.then(() => writeAtomic(cached, file.contents)).catch(() => {}))
|
|
108
|
+
}
|
|
109
|
+
stream.push(file)
|
|
110
|
+
})
|
|
111
|
+
plugin.on('error', error => stream.destroy(error))
|
|
112
|
+
|
|
113
|
+
return stream
|
|
114
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { Readable, Transform } from 'stream'
|
|
4
|
+
import * as setUp from '../test-helpers/setUp.mjs'
|
|
5
|
+
import { cacheFiles } from './cacheFiles.mjs'
|
|
6
|
+
|
|
7
|
+
setUp.setDefaults('test-cache-files')
|
|
8
|
+
const gulpConfig = setUp.gulpConfig
|
|
9
|
+
|
|
10
|
+
beforeEach(setUp.beforeEach)
|
|
11
|
+
|
|
12
|
+
afterEach(setUp.afterEach)
|
|
13
|
+
|
|
14
|
+
// Just enough of a Vinyl file for cacheFiles and the fake plugin
|
|
15
|
+
const file = (filePath, text) => ({
|
|
16
|
+
path: filePath,
|
|
17
|
+
contents: text === null ? null : Buffer.from(text),
|
|
18
|
+
isBuffer () {
|
|
19
|
+
return Buffer.isBuffer(this.contents)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// A plugin that upper-cases each file, counting the files it's given
|
|
24
|
+
const upperCase = () => {
|
|
25
|
+
const plugin = new Transform({
|
|
26
|
+
objectMode: true,
|
|
27
|
+
transform (input, encoding, callback) {
|
|
28
|
+
plugin.calls++
|
|
29
|
+
input.contents = Buffer.from(input.contents.toString().toUpperCase())
|
|
30
|
+
callback(null, input)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
plugin.calls = 0
|
|
34
|
+
return plugin
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Run files through cacheFiles and collect what comes out, as text by path
|
|
38
|
+
const run = (files, plugin, options) => new Promise((resolve, reject) => {
|
|
39
|
+
const out = {}
|
|
40
|
+
Readable.from(files)
|
|
41
|
+
.pipe(cacheFiles(plugin, options))
|
|
42
|
+
.on('data', output => {
|
|
43
|
+
out[path.basename(output.path)] = output.contents === null ? null : output.contents.toString()
|
|
44
|
+
})
|
|
45
|
+
.on('end', () => resolve(out))
|
|
46
|
+
.on('error', reject)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const cacheDir = () => `${gulpConfig.get('rootPath')}cache`
|
|
50
|
+
|
|
51
|
+
describe('cacheFiles', () => {
|
|
52
|
+
test('a file it hasn\'t seen goes through the plugin, and the result is cached', async () => {
|
|
53
|
+
const plugin = upperCase()
|
|
54
|
+
expect(await run([file('/a.txt', 'one'), file('/b.txt', 'two')], plugin, { directory: cacheDir() }))
|
|
55
|
+
.toEqual({ 'a.txt': 'ONE', 'b.txt': 'TWO' })
|
|
56
|
+
expect(plugin.calls).toBe(2)
|
|
57
|
+
expect(fs.readdirSync(path.join(cacheDir(), 'default'))).toHaveLength(2)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('the same contents again come from the cache, without the plugin', async () => {
|
|
61
|
+
await run([file('/a.txt', 'one')], upperCase(), { directory: cacheDir() })
|
|
62
|
+
const plugin = upperCase()
|
|
63
|
+
// Another path with the same contents is the same result
|
|
64
|
+
expect(await run([file('/a.txt', 'one'), file('/copy.txt', 'one')], plugin, { directory: cacheDir() }))
|
|
65
|
+
.toEqual({ 'a.txt': 'ONE', 'copy.txt': 'ONE' })
|
|
66
|
+
expect(plugin.calls).toBe(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('changed contents, or another cache name, go through the plugin again', async () => {
|
|
70
|
+
await run([file('/a.txt', 'one')], upperCase(), { directory: cacheDir() })
|
|
71
|
+
const changed = upperCase()
|
|
72
|
+
expect(await run([file('/a.txt', 'one!')], changed, { directory: cacheDir() })).toEqual({ 'a.txt': 'ONE!' })
|
|
73
|
+
expect(changed.calls).toBe(1)
|
|
74
|
+
const otherName = upperCase()
|
|
75
|
+
expect(await run([file('/a.txt', 'one')], otherName, { directory: cacheDir(), name: 'other' })).toEqual({ 'a.txt': 'ONE' })
|
|
76
|
+
expect(otherName.calls).toBe(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('another fingerprint (say, a new plugin version) goes through the plugin again', async () => {
|
|
80
|
+
await run([file('/a.txt', 'one')], upperCase(), { directory: cacheDir(), fingerprint: 'v1' })
|
|
81
|
+
const sameVersion = upperCase()
|
|
82
|
+
await run([file('/a.txt', 'one')], sameVersion, { directory: cacheDir(), fingerprint: 'v1' })
|
|
83
|
+
expect(sameVersion.calls).toBe(0)
|
|
84
|
+
const newVersion = upperCase()
|
|
85
|
+
await run([file('/a.txt', 'one')], newVersion, { directory: cacheDir(), fingerprint: 'v2' })
|
|
86
|
+
expect(newVersion.calls).toBe(1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('a plugin that renames files has its results cached, and a cached file is renamed the same way', async () => {
|
|
90
|
+
const renaming = () => {
|
|
91
|
+
const plugin = upperCase()
|
|
92
|
+
const transform = plugin._transform.bind(plugin)
|
|
93
|
+
plugin._transform = (input, encoding, callback) => transform(input, encoding, (error, output) => {
|
|
94
|
+
output.path = output.path.replace('.txt', '.md')
|
|
95
|
+
callback(error, output)
|
|
96
|
+
})
|
|
97
|
+
return plugin
|
|
98
|
+
}
|
|
99
|
+
expect(await run([file('/a.txt', 'one')], renaming(), { directory: cacheDir() })).toEqual({ 'a.md': 'ONE' })
|
|
100
|
+
const again = renaming()
|
|
101
|
+
// From the cache, renamed the same way, without the plugin
|
|
102
|
+
expect(await run([file('/a.txt', 'one')], again, { directory: cacheDir() })).toEqual({ 'a.md': 'ONE' })
|
|
103
|
+
expect(again.calls).toBe(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a plugin that makes new file objects still works; its results just aren\'t cached', async () => {
|
|
107
|
+
const copying = new Transform({
|
|
108
|
+
objectMode: true,
|
|
109
|
+
transform (input, encoding, callback) {
|
|
110
|
+
callback(null, file(input.path, input.contents.toString().toUpperCase()))
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
expect(await run([file('/a.txt', 'one')], copying, { directory: cacheDir() })).toEqual({ 'a.txt': 'ONE' })
|
|
114
|
+
expect(fs.existsSync(path.join(cacheDir(), 'default'))).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('a file without contents (a directory) passes straight through', async () => {
|
|
118
|
+
const plugin = upperCase()
|
|
119
|
+
expect(await run([file('/folder', null)], plugin, { directory: cacheDir() })).toEqual({ folder: null })
|
|
120
|
+
expect(plugin.calls).toBe(0)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('a cache that can\'t be written still gives the plugin\'s results', async () => {
|
|
124
|
+
// A file where the cache directory should be: nothing can be written inside it
|
|
125
|
+
fs.writeFileSync(cacheDir(), 'not a directory')
|
|
126
|
+
const plugin = upperCase()
|
|
127
|
+
expect(await run([file('/a.txt', 'one')], plugin, { directory: cacheDir() })).toEqual({ 'a.txt': 'ONE' })
|
|
128
|
+
expect(plugin.calls).toBe(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('two runs at once into the same cache both get every result (the cache file is written whole)', async () => {
|
|
132
|
+
const files = () => Array.from({ length: 20 }, (_, index) => file(`/f${index}.txt`, `file ${index}`))
|
|
133
|
+
const expected = Object.fromEntries(Array.from({ length: 20 }, (_, index) => [`f${index}.txt`, `FILE ${index}`]))
|
|
134
|
+
const results = await Promise.all([
|
|
135
|
+
run(files(), upperCase(), { directory: cacheDir() }),
|
|
136
|
+
run(files(), upperCase(), { directory: cacheDir() })
|
|
137
|
+
])
|
|
138
|
+
expect(results).toEqual([expected, expected])
|
|
139
|
+
expect(fs.readdirSync(path.join(cacheDir(), 'default')).filter(name => name.endsWith('.tmp'))).toEqual([])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('a plugin error fails the stream', async () => {
|
|
143
|
+
const failing = new Transform({
|
|
144
|
+
objectMode: true,
|
|
145
|
+
transform (input, encoding, callback) {
|
|
146
|
+
callback(new Error('plugin failed'))
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
await expect(run([file('/a.txt', 'one')], failing, { directory: cacheDir() })).rejects.toThrow('plugin failed')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { cacheFiles } from './cacheFiles.mjs'
|
|
2
4
|
import { requirePeer } from './loadPeer.mjs'
|
|
3
5
|
import { dest, src } from 'gulp'
|
|
4
6
|
import * as gulpConfig from '../../gulp.config.mjs'
|
|
@@ -53,6 +55,62 @@ export const imageminPlugins = (jpegQuality, plugins = loadImageminPlugins()) =>
|
|
|
53
55
|
name => Promise.resolve(plugins[name]).then(makePlugin => name === 'mozjpeg' ? makePlugin({ quality: jpegQuality }) : makePlugin())
|
|
54
56
|
)
|
|
55
57
|
|
|
58
|
+
// The packages that decide imagemin's output: the wrapper and imagemin, then each plugin with the engine it runs
|
|
59
|
+
const imageminPackages = [
|
|
60
|
+
['gulp-imagemin'],
|
|
61
|
+
['imagemin'],
|
|
62
|
+
['imagemin-gifsicle', 'gifsicle'],
|
|
63
|
+
['imagemin-mozjpeg', 'mozjpeg'],
|
|
64
|
+
['imagemin-optipng', 'optipng-bin'],
|
|
65
|
+
['imagemin-svgo', 'svgo']
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find an installed package the way Node does, from a directory up through each node_modules folder. Reads the
|
|
70
|
+
* package.json directly, so it also works for ESM-only packages that don't export it.
|
|
71
|
+
* @private
|
|
72
|
+
* @param {string} name
|
|
73
|
+
* @param {string} fromDirectory
|
|
74
|
+
* @returns {{directory: string, version: string}|undefined}
|
|
75
|
+
*/
|
|
76
|
+
const findPackage = (name, fromDirectory) => {
|
|
77
|
+
for (let directory = path.resolve(fromDirectory); ; directory = path.dirname(directory)) {
|
|
78
|
+
const file = path.join(directory, 'node_modules', name, 'package.json')
|
|
79
|
+
if (existsSync(file)) {
|
|
80
|
+
return { directory: path.dirname(file), version: JSON.parse(readFileSync(file, 'utf8')).version }
|
|
81
|
+
}
|
|
82
|
+
if (directory === path.dirname(directory)) {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The installed versions of the packages that decide imagemin's output, so the image cache stops using results made
|
|
90
|
+
* by older versions after an upgrade (see images.cacheDir).
|
|
91
|
+
* @memberOf module:partials
|
|
92
|
+
* @param {string} [projectPath=process.cwd()] - The project directory gulp-imagemin is installed in.
|
|
93
|
+
* @returns {Object<string, string>} Each package's version by name; a package that isn't installed is left out.
|
|
94
|
+
*/
|
|
95
|
+
export const imageminVersions = (projectPath = process.cwd()) => {
|
|
96
|
+
const gulpImagemin = findPackage('gulp-imagemin', projectPath)
|
|
97
|
+
const versions = {}
|
|
98
|
+
if (!gulpImagemin) {
|
|
99
|
+
return versions
|
|
100
|
+
}
|
|
101
|
+
for (const [wrapper, engine] of imageminPackages) {
|
|
102
|
+
const found = findPackage(wrapper, gulpImagemin.directory)
|
|
103
|
+
if (found) {
|
|
104
|
+
versions[wrapper] = found.version
|
|
105
|
+
const engineFound = engine && findPackage(engine, found.directory)
|
|
106
|
+
if (engineFound) {
|
|
107
|
+
versions[engine] = engineFound.version
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return versions
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
/**
|
|
57
115
|
* Move and optimize images into the browser directory.
|
|
58
116
|
* @memberOf module:partials
|
|
@@ -65,18 +123,24 @@ export const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest =
|
|
|
65
123
|
const jpegQuality = gulpConfig.get('images.jpegQuality')
|
|
66
124
|
// Left unset, imagemin runs its default plugins with their own defaults
|
|
67
125
|
const plugins = jpegQuality ? imageminPlugins(jpegQuality) : undefined
|
|
68
|
-
|
|
126
|
+
const options = {
|
|
127
|
+
interlaced: true,
|
|
128
|
+
silent: true,
|
|
129
|
+
verbose: false,
|
|
130
|
+
}
|
|
131
|
+
// Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images.
|
|
69
132
|
return src(imageSrc, { encoding: false })
|
|
70
133
|
.pipe(
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
imagemin(plugins,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
// Only images imagemin hasn't optimized before go through it; the rest come from the cache
|
|
135
|
+
cacheFiles(
|
|
136
|
+
imagemin(plugins, options),
|
|
137
|
+
{
|
|
138
|
+
directory: gulpConfig.get('images.cacheDir'),
|
|
139
|
+
// Each quality gets results of its own
|
|
140
|
+
name: jpegQuality ? `jpeg-quality-${jpegQuality}` : 'default',
|
|
141
|
+
// A new imagemin (or plugin, or engine) version, or other options, makes new results
|
|
142
|
+
fingerprint: JSON.stringify({ options, versions: imageminVersions() })
|
|
143
|
+
}
|
|
80
144
|
)
|
|
81
145
|
)
|
|
82
146
|
.pipe(dest(imageDest))
|
|
@@ -4,11 +4,9 @@ import * as setUp from '../test-helpers/setUp.mjs'
|
|
|
4
4
|
import os from 'os'
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import zlib from 'zlib'
|
|
7
|
-
import { imageminPlugins, imagesFor, loadImagemin } from './imagesFor.mjs'
|
|
8
|
-
import { isolateGulpCache } from '../test-helpers/isolateGulpCache.mjs'
|
|
7
|
+
import { imageminPlugins, imageminVersions, imagesFor, loadImagemin } from './imagesFor.mjs'
|
|
9
8
|
|
|
10
9
|
setUp.setDefaults('test-images-for')
|
|
11
|
-
const clearImageCache = isolateGulpCache('images-for')
|
|
12
10
|
const gulpConfig = setUp.gulpConfig
|
|
13
11
|
|
|
14
12
|
const imageContents = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
|
|
@@ -179,8 +177,8 @@ const optimize = (file, to) => new Promise((resolve, reject) => {
|
|
|
179
177
|
.on('error', reject)
|
|
180
178
|
})
|
|
181
179
|
|
|
182
|
-
//
|
|
183
|
-
beforeEach(() =>
|
|
180
|
+
// A fresh temp directory each test, which is also an empty image cache (see setUp's images.cacheDir)
|
|
181
|
+
beforeEach(() => setUp.beforeEach())
|
|
184
182
|
|
|
185
183
|
afterEach(setUp.afterEach)
|
|
186
184
|
|
|
@@ -231,6 +229,32 @@ describe('imagesFor', () => {
|
|
|
231
229
|
})
|
|
232
230
|
})
|
|
233
231
|
|
|
232
|
+
describe('images.cacheDir', () => {
|
|
233
|
+
test('a second run takes the optimized image from the cache in images.cacheDir', async () => {
|
|
234
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
235
|
+
const imagesFile = `${srcPath}/cached.svg`
|
|
236
|
+
fs.writeFileSync(imagesFile, imageContents)
|
|
237
|
+
const browserPath = gulpConfig.get('browser.to')
|
|
238
|
+
expect((await optimize(imagesFile, browserPath)).toString()).toEqual(imageCompressed)
|
|
239
|
+
const cacheDir = path.join(gulpConfig.get('images.cacheDir'), 'default')
|
|
240
|
+
const entries = fs.readdirSync(cacheDir)
|
|
241
|
+
expect(entries).toHaveLength(1)
|
|
242
|
+
// Change the cached result: the second run can only produce this by reading the cache instead of imagemin
|
|
243
|
+
fs.writeFileSync(path.join(cacheDir, entries[0]), '<svg>from the cache</svg>')
|
|
244
|
+
expect((await optimize(imagesFile, browserPath)).toString()).toEqual('<svg>from the cache</svg>')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('the cache key includes the installed imagemin packages\' versions', () => {
|
|
248
|
+
const versions = imageminVersions()
|
|
249
|
+
expect(Object.keys(versions)).toEqual(expect.arrayContaining(['gulp-imagemin', 'imagemin', 'imagemin-svgo', 'svgo', 'imagemin-mozjpeg', 'mozjpeg']))
|
|
250
|
+
expect(versions['gulp-imagemin']).toMatch(/^\d+\.\d+\.\d+/)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('no gulp-imagemin to find gives no versions', () => {
|
|
254
|
+
expect(imageminVersions(os.tmpdir())).toEqual({})
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
234
258
|
describe('images.jpegQuality', () => {
|
|
235
259
|
test('compresses JPEGs to the configured quality, never reusing another quality\'s cached result', async () => {
|
|
236
260
|
expect.assertions(4)
|
package/functions/partials.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
export { addToReadme } from './partials/addToReadme.mjs'
|
|
11
11
|
export { beginWatcher } from './partials/beginWatcher.mjs'
|
|
12
|
+
export { cacheFiles } from './partials/cacheFiles.mjs'
|
|
12
13
|
export { clean } from './partials/clean.mjs'
|
|
13
14
|
export { copyFor } from './partials/copyFor.mjs'
|
|
14
15
|
export { distFor } from './partials/distFor.mjs'
|
|
@@ -31,6 +31,8 @@ export const setDefaults = (testDir = 'test-temp') => {
|
|
|
31
31
|
gulpConfig.set('docs.title', '')
|
|
32
32
|
gulpConfig.set('fonts.from', `${srcPath}/fonts/**/*`)
|
|
33
33
|
gulpConfig.set('fonts.to', `${browserPath}/fonts`)
|
|
34
|
+
// Each test file's own cache, inside its temp directory, so beforeEach's fresh directory is also an empty cache
|
|
35
|
+
gulpConfig.set('images.cacheDir', `${tempDir}image-cache`)
|
|
34
36
|
gulpConfig.set('images.from', `${srcPath}/img/**/*.+(png|jpg|jpeg|gif|svg)`)
|
|
35
37
|
gulpConfig.set('images.to', `${browserPath}/img`)
|
|
36
38
|
gulpConfig.set('readme.template', `${tempDir}MAIN.md`)
|
package/gulp.config.mjs
CHANGED
|
@@ -105,6 +105,10 @@ import { fileExists } from 'test-filesystem'
|
|
|
105
105
|
/**
|
|
106
106
|
* Configurations to minify and copy the images.
|
|
107
107
|
* @typedef {Object<string, Setting>} module:gulpConfig~ImageConfig
|
|
108
|
+
* @property {StringSetting} cacheDir - Where optimized images are cached between runs, so an image that hasn't
|
|
109
|
+
* changed isn't optimized again. An upgrade of imagemin (or its plugins) or other settings starts new results. Inside
|
|
110
|
+
* node_modules by default, so it's never committed; old entries aren't pruned, so delete it now and then to start
|
|
111
|
+
* over.
|
|
108
112
|
* @property {BooleanSetting} enabled - Toggle image minify and copy process.
|
|
109
113
|
* @property {StringSetting} from - Path to search for images.
|
|
110
114
|
* @property {FlagNumberSetting} jpegQuality - The JPEG quality (0 to 100) mozjpeg compresses to. False (the
|
|
@@ -250,6 +254,7 @@ const setDefaults = {
|
|
|
250
254
|
to: 'browser/fonts',
|
|
251
255
|
},
|
|
252
256
|
images: {
|
|
257
|
+
cacheDir: 'node_modules/.cache/js-build-tools/images',
|
|
253
258
|
enabled: false,
|
|
254
259
|
from: 'src/img/**/*.+(png|jpg|jpeg|gif|svg)',
|
|
255
260
|
jpegQuality: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js-build-tools",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "Standardised build process with gulp.",
|
|
5
5
|
"main": "gulpfile.base.js",
|
|
6
6
|
"exports": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dev": "gulp",
|
|
23
23
|
"docs": "gulp docs",
|
|
24
24
|
"lint": "gulp lint",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
25
26
|
"readme": "gulp readme",
|
|
26
27
|
"release": "release-it",
|
|
27
28
|
"release:dry-run": "release-it --dry-run",
|
|
@@ -60,10 +61,9 @@
|
|
|
60
61
|
"core-js": "^3.51.0",
|
|
61
62
|
"cssnano": "^6.1.2",
|
|
62
63
|
"eslint": "^10.13.0",
|
|
63
|
-
"glob": "^
|
|
64
|
+
"glob": "^14.0.2",
|
|
64
65
|
"gulp": "^5.0.1",
|
|
65
66
|
"gulp-babel": "^8.1.0",
|
|
66
|
-
"gulp-cache": "^1.1.3",
|
|
67
67
|
"gulp-postcss": "^10.0.0",
|
|
68
68
|
"gulp-rename": "^2.1.0",
|
|
69
69
|
"gulp-sass": "^6.0.1",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.isolateGulpCache = void 0;
|
|
7
|
-
var _gulpCache = _interopRequireDefault(require("gulp-cache"));
|
|
8
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
/**
|
|
10
|
-
* Give this test file its own gulp-cache folder, and return a function that clears only that folder. gulp-cache
|
|
11
|
-
* otherwise keeps one cache in os.tmpdir() for every process, so a cache.clearAll() in one Jest worker could wipe it
|
|
12
|
-
* while another worker's images task was still using it, and gulp-cache then silently dropped that worker's
|
|
13
|
-
* images. Jest loads gulp-cache separately for each test file, so this only changes the copy that file uses.
|
|
14
|
-
* @memberOf module:testHelpers
|
|
15
|
-
* @param {string} name - Unique per test file, used in the cache folder name.
|
|
16
|
-
* @returns {function(): Promise<void>} Clears this test file's cache.
|
|
17
|
-
*/
|
|
18
|
-
const isolateGulpCache = name => {
|
|
19
|
-
const fileCache = new _gulpCache.default.Cache({
|
|
20
|
-
cacheDirName: `js-build-tools-test-${name}`
|
|
21
|
-
});
|
|
22
|
-
_gulpCache.default.defaultOptions.fileCache = fileCache;
|
|
23
|
-
return () => new Promise((resolve, reject) => fileCache.clear(null, error => error ? reject(error) : resolve()));
|
|
24
|
-
};
|
|
25
|
-
exports.isolateGulpCache = isolateGulpCache;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import cache from 'gulp-cache'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Give this test file its own gulp-cache folder, and return a function that clears only that folder. gulp-cache
|
|
5
|
-
* otherwise keeps one cache in os.tmpdir() for every process, so a cache.clearAll() in one Jest worker could wipe it
|
|
6
|
-
* while another worker's images task was still using it, and gulp-cache then silently dropped that worker's
|
|
7
|
-
* images. Jest loads gulp-cache separately for each test file, so this only changes the copy that file uses.
|
|
8
|
-
* @memberOf module:testHelpers
|
|
9
|
-
* @param {string} name - Unique per test file, used in the cache folder name.
|
|
10
|
-
* @returns {function(): Promise<void>} Clears this test file's cache.
|
|
11
|
-
*/
|
|
12
|
-
export const isolateGulpCache = name => {
|
|
13
|
-
const fileCache = new cache.Cache({ cacheDirName: `js-build-tools-test-${name}` })
|
|
14
|
-
cache.defaultOptions.fileCache = fileCache
|
|
15
|
-
return () => new Promise((resolve, reject) => fileCache.clear(null, error => error ? reject(error) : resolve()))
|
|
16
|
-
}
|