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 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
- var _gulpCache = _interopRequireDefault(require("gulp-cache"));
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.imageminPlugins = imageminPlugins;
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
- // Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images (and makes gulp-cache drop them).
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
- // gulp-cache keys on the file's contents alone, so keep each quality's results apart
90
- {
91
- name: jpegQuality ? `jpeg-quality-${jpegQuality}` : 'default'
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
- ...(indexFile && _nodeFs.default.existsSync(indexFile) ? {
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");
@@ -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`);
@@ -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
- // The cache outlives a run, so clear it or a stale result from an older imagemin can pass
256
+ // A fresh temp directory each test, which is also an empty image cache (see setUp's images.cacheDir)
259
257
  beforeEach(
260
- () => clearImageCache()
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
- // The cache outlives a run, so clear it or a stale result from an older imagemin can pass
120
- beforeEach(() => clearImageCache().then(setUp.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 cache from 'gulp-cache'
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
- // Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images (and makes gulp-cache drop them).
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
- // Caching images that ran through imagemin
72
- cache(
73
- imagemin(plugins, {
74
- interlaced: true,
75
- silent: true,
76
- verbose: false,
77
- }),
78
- // gulp-cache keys on the file's contents alone, so keep each quality's results apart
79
- { name: jpegQuality ? `jpeg-quality-${jpegQuality}` : 'default' }
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
- // The cache outlives a run, so clear it or a stale result from an older imagemin can pass
183
- beforeEach(() => clearImageCache().then(setUp.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)
@@ -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'
@@ -2,6 +2,6 @@ import * as partials from './partials.mjs'
2
2
 
3
3
  describe('partials', () => {
4
4
  test('is object with all partial exports', () => {
5
- expect(Object.keys(partials).length).toBe(25)
5
+ expect(Object.keys(partials).length).toBe(26)
6
6
  })
7
7
  })
@@ -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.7.0",
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": "^13.0.6",
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
- }