js-build-tools 6.5.0 → 6.6.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 +9 -0
- package/dist/partials/imagesFor.js +41 -4
- package/dist/partials/typeDocsFor.js +214 -208
- package/functions/partials/imagesFor.mjs +35 -2
- package/functions/partials/imagesFor.test.mjs +59 -1
- package/functions/partials/typeDocsFor.mjs +5 -1
- package/functions/partials/typeDocsFor.test.mjs +11 -0
- package/gulp.config.mjs +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,8 @@ provided are the defaults):
|
|
|
90
90
|
"enabled": false,
|
|
91
91
|
// Path to search for images
|
|
92
92
|
"from": "src/img/**/*.+(png|jpg|jpeg|gif|svg)",
|
|
93
|
+
// The JPEG quality (0 to 100) to compress to, false keeps mozjpeg's own default (75)
|
|
94
|
+
"jpegQuality": false,
|
|
93
95
|
// Path to output images
|
|
94
96
|
"to": "browser/img"
|
|
95
97
|
},
|
|
@@ -439,6 +441,9 @@ Add the following to the exports in your `build-tools.config.json`:
|
|
|
439
441
|
"enabled": false,
|
|
440
442
|
// Search pattern to find your images
|
|
441
443
|
"from": "src/img/**/*.+(png|jpg|jpeg|gif|svg)",
|
|
444
|
+
// The JPEG quality (0 to 100) to compress to. Left false, mozjpeg uses its own default of 75, which can
|
|
445
|
+
// visibly band smooth gradients in photos; 85 is usually indistinguishable from the original
|
|
446
|
+
"jpegQuality": false,
|
|
442
447
|
// Output directory for your images files
|
|
443
448
|
"to": "browser/img"
|
|
444
449
|
}
|
|
@@ -561,6 +566,10 @@ Add the following to the exports in your `build-tools.config.json`:
|
|
|
561
566
|
// The directory holding the source, each folder in it becomes a module (TypeDoc) or is scanned recursively (jsdoc).
|
|
562
567
|
// jsdoc also takes a list of directories and files, for example ["functions", "gulpfile.base.mjs"]
|
|
563
568
|
"from": "src",
|
|
569
|
+
// TypeDoc only: the git revision the "Defined in" source links point to. Left false, TypeDoc uses the current
|
|
570
|
+
// commit, so every commit changes every page; a branch name such as "main" keeps them the same until the
|
|
571
|
+
// documentation itself changes
|
|
572
|
+
"gitRevision": false,
|
|
564
573
|
// Generate the documentation as part of build (the default). Set it to false to only generate it when you run
|
|
565
574
|
// the docs task yourself (see below)
|
|
566
575
|
"inBuild": true,
|
|
@@ -4,7 +4,7 @@ require("core-js/modules/esnext.weak-map.delete-all.js");
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
-
exports.loadImagemin = exports.imagesFor = void 0;
|
|
7
|
+
exports.loadImageminPlugins = exports.loadImagemin = exports.imagesFor = exports.imageminPlugins = void 0;
|
|
8
8
|
var _gulpCache = _interopRequireDefault(require("gulp-cache"));
|
|
9
9
|
var _loadPeer = require("./loadPeer.js");
|
|
10
10
|
var _gulp = require("gulp");
|
|
@@ -33,6 +33,36 @@ const loadImagemin = (projectPath = process.cwd()) => {
|
|
|
33
33
|
return loaded && (loaded.__esModule || loaded.default) ? loaded.default : loaded;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Load the plugins `gulp-imagemin` exports by name (gifsicle, mozjpeg, optipng and svgo), each a promise of a plugin
|
|
38
|
+
* factory, for when they need options of their own (see images.jpegQuality). Resolved like {@link loadImagemin}.
|
|
39
|
+
* @memberOf module:partials
|
|
40
|
+
* @param {string} [projectPath=process.cwd()] - The project directory to resolve `gulp-imagemin` from.
|
|
41
|
+
* @returns {Object<string, Promise<Function>>} The plugin factories, by name.
|
|
42
|
+
*/
|
|
43
|
+
exports.loadImagemin = loadImagemin;
|
|
44
|
+
const loadImageminPlugins = (projectPath = process.cwd()) => (0, _loadPeer.requirePeer)('gulp-imagemin', {
|
|
45
|
+
task: 'images',
|
|
46
|
+
installHint,
|
|
47
|
+
esmOnlyMessage: `gulp-imagemin could not be loaded because it is ESM-only. ${installHint}`,
|
|
48
|
+
projectPath
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// The plugins imagemin runs when it isn't given any, in its own order
|
|
52
|
+
exports.loadImageminPlugins = loadImageminPlugins;
|
|
53
|
+
const defaultPluginNames = ['gifsicle', 'mozjpeg', 'optipng', 'svgo'];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The plugins imagemin uses by default, with mozjpeg set to the given JPEG quality instead of its own default (75).
|
|
57
|
+
* @memberOf module:partials
|
|
58
|
+
* @param {number} jpegQuality - The mozjpeg quality, 0 to 100.
|
|
59
|
+
* @param {Object<string, Promise<Function>>} [plugins=loadImageminPlugins()] - The plugin factories, by name.
|
|
60
|
+
* @returns {Array<Promise<Function>>} The plugins, in imagemin's default order.
|
|
61
|
+
*/
|
|
62
|
+
const imageminPlugins = (jpegQuality, plugins = loadImageminPlugins()) => defaultPluginNames.map(name => Promise.resolve(plugins[name]).then(makePlugin => name === 'mozjpeg' ? makePlugin({
|
|
63
|
+
quality: jpegQuality
|
|
64
|
+
}) : makePlugin()));
|
|
65
|
+
|
|
36
66
|
/**
|
|
37
67
|
* Move and optimize images into the browser directory.
|
|
38
68
|
* @memberOf module:partials
|
|
@@ -40,18 +70,25 @@ const loadImagemin = (projectPath = process.cwd()) => {
|
|
|
40
70
|
* @param {string} [imageDest=dest/image/folder]
|
|
41
71
|
* @return {stream.Stream}
|
|
42
72
|
*/
|
|
43
|
-
exports.
|
|
73
|
+
exports.imageminPlugins = imageminPlugins;
|
|
44
74
|
const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest = gulpConfig.get('images.to')) => {
|
|
45
75
|
const imagemin = loadImagemin();
|
|
76
|
+
const jpegQuality = gulpConfig.get('images.jpegQuality');
|
|
77
|
+
// Left unset, imagemin runs its default plugins with their own defaults
|
|
78
|
+
const plugins = jpegQuality ? imageminPlugins(jpegQuality) : undefined;
|
|
46
79
|
// Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images (and makes gulp-cache drop them).
|
|
47
80
|
return (0, _gulp.src)(imageSrc, {
|
|
48
81
|
encoding: false
|
|
49
82
|
}).pipe(
|
|
50
83
|
// Caching images that ran through imagemin
|
|
51
|
-
(0, _gulpCache.default)(imagemin({
|
|
84
|
+
(0, _gulpCache.default)(imagemin(plugins, {
|
|
52
85
|
interlaced: true,
|
|
53
86
|
silent: true,
|
|
54
87
|
verbose: false
|
|
55
|
-
})
|
|
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'
|
|
92
|
+
})).pipe((0, _gulp.dest)(imageDest));
|
|
56
93
|
};
|
|
57
94
|
exports.imagesFor = imagesFor;
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
require("core-js/modules/esnext.weak-map.delete-all.js");
|
|
4
|
-
Object.defineProperty(exports, "__esModule", {
|
|
5
|
-
value: true
|
|
6
|
-
});
|
|
7
|
-
exports.writeDocEntries = exports.typeDocsFor = exports.loadTypeDoc = exports.describeFolder = exports.describeFile = exports.describeExports = void 0;
|
|
8
|
-
require("core-js/modules/es.array.includes.js");
|
|
9
|
-
require("core-js/modules/es.json.stringify.js");
|
|
10
|
-
require("core-js/modules/esnext.iterator.constructor.js");
|
|
11
|
-
require("core-js/modules/esnext.iterator.filter.js");
|
|
12
|
-
require("core-js/modules/esnext.iterator.find.js");
|
|
13
|
-
require("core-js/modules/esnext.iterator.flat-map.js");
|
|
14
|
-
require("core-js/modules/esnext.iterator.for-each.js");
|
|
15
|
-
require("core-js/modules/esnext.iterator.map.js");
|
|
16
|
-
require("core-js/modules/esnext.iterator.some.js");
|
|
17
|
-
var _nodeFs = _interopRequireDefault(require("node:fs"));
|
|
18
|
-
var _nodeOs = _interopRequireDefault(require("node:os"));
|
|
19
|
-
var _nodePath = _interopRequireDefault(require("node:path"));
|
|
20
|
-
var gulpConfig = _interopRequireWildcard(require("../../gulp.config.js"));
|
|
21
|
-
var _loadPeer = require("./loadPeer.js");
|
|
22
|
-
var _tsFor = require("./tsFor.js");
|
|
23
|
-
var _removeDirectory = require("./removeDirectory.js");
|
|
24
|
-
var cache;
|
|
25
|
-
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
|
26
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
27
|
-
const installHint = 'Install it in your project with: npm install --save-dev typedoc';
|
|
28
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
require("core-js/modules/esnext.weak-map.delete-all.js");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.writeDocEntries = exports.typeDocsFor = exports.loadTypeDoc = exports.describeFolder = exports.describeFile = exports.describeExports = void 0;
|
|
8
|
+
require("core-js/modules/es.array.includes.js");
|
|
9
|
+
require("core-js/modules/es.json.stringify.js");
|
|
10
|
+
require("core-js/modules/esnext.iterator.constructor.js");
|
|
11
|
+
require("core-js/modules/esnext.iterator.filter.js");
|
|
12
|
+
require("core-js/modules/esnext.iterator.find.js");
|
|
13
|
+
require("core-js/modules/esnext.iterator.flat-map.js");
|
|
14
|
+
require("core-js/modules/esnext.iterator.for-each.js");
|
|
15
|
+
require("core-js/modules/esnext.iterator.map.js");
|
|
16
|
+
require("core-js/modules/esnext.iterator.some.js");
|
|
17
|
+
var _nodeFs = _interopRequireDefault(require("node:fs"));
|
|
18
|
+
var _nodeOs = _interopRequireDefault(require("node:os"));
|
|
19
|
+
var _nodePath = _interopRequireDefault(require("node:path"));
|
|
20
|
+
var gulpConfig = _interopRequireWildcard(require("../../gulp.config.js"));
|
|
21
|
+
var _loadPeer = require("./loadPeer.js");
|
|
22
|
+
var _tsFor = require("./tsFor.js");
|
|
23
|
+
var _removeDirectory = require("./removeDirectory.js");
|
|
24
|
+
var cache;
|
|
25
|
+
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
|
26
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
27
|
+
const installHint = 'Install it in your project with: npm install --save-dev typedoc';
|
|
28
|
+
|
|
29
29
|
/**
|
|
30
30
|
* Find the TypeDoc of the project being built (an optional peer dependency, only needed when the docs task is
|
|
31
31
|
* enabled) and load it, so projects which do not document TypeScript never need it installed.
|
|
@@ -33,87 +33,87 @@ const installHint = 'Install it in your project with: npm install --save-dev typ
|
|
|
33
33
|
* @param {string} [projectPath=process.cwd()] The folder of the project (which has the package.json) being built.
|
|
34
34
|
* @returns {Promise<Object>} The TypeDoc module.
|
|
35
35
|
* @throws {Error} With install instructions when typedoc is not installed.
|
|
36
|
-
*/
|
|
37
|
-
const loadTypeDoc = (projectPath = process.cwd()) => (0, _loadPeer.importPeer)('typedoc', {
|
|
38
|
-
task: 'docs',
|
|
39
|
-
installHint,
|
|
40
|
-
projectPath
|
|
41
|
-
});
|
|
42
|
-
exports.loadTypeDoc = loadTypeDoc;
|
|
43
|
-
const isSource = fileName => /\.(ts|tsx|mts|cts)$/.test(fileName) && !/\.d\.[cm]?ts$/.test(fileName) && !/\.(test|spec)\./.test(fileName);
|
|
44
|
-
const listSources = dirPath => _nodeFs.default.readdirSync(dirPath, {
|
|
45
|
-
withFileTypes: true
|
|
46
|
-
}).filter(entry => entry.isFile() && isSource(entry.name)).map(entry => _nodePath.default.join(dirPath, entry.name));
|
|
47
|
-
|
|
48
|
-
// Every folder below dirPath (at any depth), named by its path from dirPath, with the source files directly in it
|
|
49
|
-
const listFolders = (dirPath, prefix = '') => _nodeFs.default.readdirSync(dirPath, {
|
|
50
|
-
withFileTypes: true
|
|
51
|
-
}).filter(entry => entry.isDirectory() && entry.name !== 'node_modules').flatMap(entry => {
|
|
52
|
-
const folderPath = _nodePath.default.join(dirPath, entry.name);
|
|
53
|
-
const name = `${prefix}${entry.name}`;
|
|
54
|
-
return [{
|
|
55
|
-
name,
|
|
56
|
-
path: folderPath,
|
|
57
|
-
files: listSources(folderPath)
|
|
58
|
-
}, ...listFolders(folderPath, `${name}/`)];
|
|
59
|
-
});
|
|
60
|
-
const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1');
|
|
61
|
-
const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(_nodePath.default.sep).join('/');
|
|
62
|
-
|
|
36
|
+
*/
|
|
37
|
+
const loadTypeDoc = (projectPath = process.cwd()) => (0, _loadPeer.importPeer)('typedoc', {
|
|
38
|
+
task: 'docs',
|
|
39
|
+
installHint,
|
|
40
|
+
projectPath
|
|
41
|
+
});
|
|
42
|
+
exports.loadTypeDoc = loadTypeDoc;
|
|
43
|
+
const isSource = fileName => /\.(ts|tsx|mts|cts)$/.test(fileName) && !/\.d\.[cm]?ts$/.test(fileName) && !/\.(test|spec)\./.test(fileName);
|
|
44
|
+
const listSources = dirPath => _nodeFs.default.readdirSync(dirPath, {
|
|
45
|
+
withFileTypes: true
|
|
46
|
+
}).filter(entry => entry.isFile() && isSource(entry.name)).map(entry => _nodePath.default.join(dirPath, entry.name));
|
|
47
|
+
|
|
48
|
+
// Every folder below dirPath (at any depth), named by its path from dirPath, with the source files directly in it
|
|
49
|
+
const listFolders = (dirPath, prefix = '') => _nodeFs.default.readdirSync(dirPath, {
|
|
50
|
+
withFileTypes: true
|
|
51
|
+
}).filter(entry => entry.isDirectory() && entry.name !== 'node_modules').flatMap(entry => {
|
|
52
|
+
const folderPath = _nodePath.default.join(dirPath, entry.name);
|
|
53
|
+
const name = `${prefix}${entry.name}`;
|
|
54
|
+
return [{
|
|
55
|
+
name,
|
|
56
|
+
path: folderPath,
|
|
57
|
+
files: listSources(folderPath)
|
|
58
|
+
}, ...listFolders(folderPath, `${name}/`)];
|
|
59
|
+
});
|
|
60
|
+
const toIdentifier = fileName => fileName.replace(/[^\w$]+(\w)?/g, (match, letter) => (letter || '').toUpperCase()).replace(/^(\d)/, '_$1');
|
|
61
|
+
const toModulePath = filePath => filePath.replace(/\.[cm]?tsx?$/, '').split(_nodePath.default.sep).join('/');
|
|
62
|
+
|
|
63
63
|
/**
|
|
64
64
|
* Describe what a source file exports: whether it has a default export, and whether it has any named exports.
|
|
65
65
|
* @memberOf module:partials
|
|
66
66
|
* @param {Object} ts - The TypeScript compiler API.
|
|
67
67
|
* @param {string} filePath
|
|
68
68
|
* @returns {{hasDefault: boolean, hasNamed: boolean}}
|
|
69
|
-
*/
|
|
70
|
-
const describeExports = (ts, filePath) => {
|
|
71
|
-
const source = ts.createSourceFile(filePath, _nodeFs.default.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest);
|
|
72
|
-
let hasDefault = false;
|
|
73
|
-
let hasNamed = false;
|
|
74
|
-
source.statements.forEach(statement => {
|
|
75
|
-
if (ts.isExportAssignment(statement)) {
|
|
76
|
-
hasDefault = hasDefault || !statement.isExportEquals;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (ts.isExportDeclaration(statement)) {
|
|
80
|
-
const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null;
|
|
81
|
-
if (names && names.some(element => element.name.text === 'default')) {
|
|
82
|
-
hasDefault = true;
|
|
83
|
-
}
|
|
84
|
-
if (!names || names.some(element => element.name.text !== 'default')) {
|
|
85
|
-
hasNamed = true;
|
|
86
|
-
}
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : [];
|
|
90
|
-
if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
91
|
-
if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
92
|
-
hasDefault = true;
|
|
93
|
-
} else {
|
|
94
|
-
hasNamed = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
return {
|
|
99
|
-
hasDefault,
|
|
100
|
-
hasNamed
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
|
|
69
|
+
*/
|
|
70
|
+
const describeExports = (ts, filePath) => {
|
|
71
|
+
const source = ts.createSourceFile(filePath, _nodeFs.default.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest);
|
|
72
|
+
let hasDefault = false;
|
|
73
|
+
let hasNamed = false;
|
|
74
|
+
source.statements.forEach(statement => {
|
|
75
|
+
if (ts.isExportAssignment(statement)) {
|
|
76
|
+
hasDefault = hasDefault || !statement.isExportEquals;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (ts.isExportDeclaration(statement)) {
|
|
80
|
+
const names = statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : null;
|
|
81
|
+
if (names && names.some(element => element.name.text === 'default')) {
|
|
82
|
+
hasDefault = true;
|
|
83
|
+
}
|
|
84
|
+
if (!names || names.some(element => element.name.text !== 'default')) {
|
|
85
|
+
hasNamed = true;
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) || [] : [];
|
|
90
|
+
if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
91
|
+
if (modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
92
|
+
hasDefault = true;
|
|
93
|
+
} else {
|
|
94
|
+
hasNamed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
hasDefault,
|
|
100
|
+
hasNamed
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
104
|
/**
|
|
105
105
|
* The description of a folder for its module: the first doc comment of the folder's index file, without its tags
|
|
106
106
|
* (@module, @file, ...), or an empty string when there is none.
|
|
107
107
|
* @memberOf module:partials
|
|
108
108
|
* @param {string} folderPath
|
|
109
109
|
* @returns {string}
|
|
110
|
-
*/
|
|
111
|
-
exports.describeExports = describeExports;
|
|
112
|
-
const describeFolder = folderPath => {
|
|
113
|
-
const indexFile = ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(name => _nodePath.default.join(folderPath, name)).find(file => _nodeFs.default.existsSync(file));
|
|
114
|
-
return indexFile ? describeFile(indexFile) : '';
|
|
115
|
-
};
|
|
116
|
-
|
|
110
|
+
*/
|
|
111
|
+
exports.describeExports = describeExports;
|
|
112
|
+
const describeFolder = folderPath => {
|
|
113
|
+
const indexFile = ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(name => _nodePath.default.join(folderPath, name)).find(file => _nodeFs.default.existsSync(file));
|
|
114
|
+
return indexFile ? describeFile(indexFile) : '';
|
|
115
|
+
};
|
|
116
|
+
|
|
117
117
|
/**
|
|
118
118
|
* The description of a source file for its module: its first doc comment without the tags (@module, @file, ...), when
|
|
119
119
|
* that comment is a header (it is followed by an import or export, so it does not document a declaration itself), or
|
|
@@ -121,18 +121,18 @@ const describeFolder = folderPath => {
|
|
|
121
121
|
* @memberOf module:partials
|
|
122
122
|
* @param {string} filePath
|
|
123
123
|
* @returns {string}
|
|
124
|
-
*/
|
|
125
|
-
exports.describeFolder = describeFolder;
|
|
126
|
-
const describeFile = filePath => {
|
|
127
|
-
const comment = _nodeFs.default.readFileSync(filePath, 'utf8').match(/^\s*\/\*\*([\s\S]*?)\*\/\s*(import\b|export\s+(\*|\{[^}]*\}\s+from)|$)/);
|
|
128
|
-
if (!comment) {
|
|
129
|
-
return '';
|
|
130
|
-
}
|
|
131
|
-
const lines = comment[1].split('\n').map(line => line.replace(/^\s*\* ?/, '').trimEnd());
|
|
132
|
-
const tagAt = lines.findIndex(line => /^@\w+/.test(line));
|
|
133
|
-
return (tagAt === -1 ? lines : lines.slice(0, tagAt)).join('\n').trim();
|
|
134
|
-
};
|
|
135
|
-
|
|
124
|
+
*/
|
|
125
|
+
exports.describeFolder = describeFolder;
|
|
126
|
+
const describeFile = filePath => {
|
|
127
|
+
const comment = _nodeFs.default.readFileSync(filePath, 'utf8').match(/^\s*\/\*\*([\s\S]*?)\*\/\s*(import\b|export\s+(\*|\{[^}]*\}\s+from)|$)/);
|
|
128
|
+
if (!comment) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
const lines = comment[1].split('\n').map(line => line.replace(/^\s*\* ?/, '').trimEnd());
|
|
132
|
+
const tagAt = lines.findIndex(line => /^@\w+/.test(line));
|
|
133
|
+
return (tagAt === -1 ? lines : lines.slice(0, tagAt)).join('\n').trim();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
136
|
/**
|
|
137
137
|
* Write one entry file per folder of the source directory into the entry directory. TypeDoc makes a module of every
|
|
138
138
|
* entry file, so the modules of the documentation mirror the folders. Nested folders are modules of their own, named
|
|
@@ -147,58 +147,58 @@ const describeFile = filePath => {
|
|
|
147
147
|
* @param {string} srcDir - The directory holding the TypeScript source.
|
|
148
148
|
* @param {string} entryDir - The directory to write the entry files into.
|
|
149
149
|
* @returns {Array<string>} The paths of the entry files.
|
|
150
|
-
*/
|
|
151
|
-
exports.describeFile = describeFile;
|
|
152
|
-
const writeDocEntries = (ts, srcDir, entryDir) => {
|
|
153
|
-
const absoluteSrc = _nodePath.default.resolve(srcDir);
|
|
154
|
-
_nodeFs.default.mkdirSync(entryDir, {
|
|
155
|
-
recursive: true
|
|
156
|
-
});
|
|
157
|
-
const folders = listFolders(absoluteSrc).filter(folder => folder.files.some(file => !/^index\./.test(_nodePath.default.basename(file))));
|
|
158
|
-
const rootFiles = listSources(absoluteSrc).filter(file => !/^(index|main|browser)\./.test(_nodePath.default.basename(file)));
|
|
159
|
-
// Folders make modules, and so do the files which sit beside them (except the barrels: index, main and browser, the
|
|
160
|
-
// browser bundle's entry). A source directory with no folders gets a single module called index which holds its files.
|
|
161
|
-
const groups = folders.length ? [...folders, ...rootFiles.map(file => ({
|
|
162
|
-
name: toIdentifier(_nodePath.default.basename(file).replace(/\.[cm]?tsx?$/, '')),
|
|
163
|
-
files: [file],
|
|
164
|
-
description: describeFile(file)
|
|
165
|
-
})).filter(group => !folders.some(folder => folder.name === group.name))] : [{
|
|
166
|
-
name: 'index',
|
|
167
|
-
path: absoluteSrc,
|
|
168
|
-
files: rootFiles
|
|
169
|
-
}];
|
|
170
|
-
return groups.map(group => {
|
|
171
|
-
const lines = group.files.filter(file => !/^index\./.test(_nodePath.default.basename(file))).sort().flatMap(file => {
|
|
172
|
-
const modulePath = toModulePath(file);
|
|
173
|
-
const {
|
|
174
|
-
hasDefault,
|
|
175
|
-
hasNamed
|
|
176
|
-
} = describeExports(ts, file);
|
|
177
|
-
const fileName = _nodePath.default.basename(file).replace(/\.[cm]?tsx?$/, '');
|
|
178
|
-
return [...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []), ...(hasNamed ? [`export * from '${modulePath}'`] : [])];
|
|
179
|
-
});
|
|
180
|
-
const description = group.description ?? (group.path ? describeFolder(group.path) : '');
|
|
181
|
-
// TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
|
|
182
|
-
// A nested folder's module is named by its path (utilities/parsers), so it is not mistaken for its parent's
|
|
183
|
-
const moduleName = group.name.includes('/') ? ` ${group.name}` : '';
|
|
184
|
-
const moduleComment = description || moduleName ? `/**\n${description ? description.split('\n').map(line => ` * ${line}`.trimEnd()).join('\n') + '\n' : ''} * @module${moduleName}\n */\n\n` : '';
|
|
185
|
-
const entryPath = _nodePath.default.join(entryDir, `${group.name}.ts`);
|
|
186
|
-
_nodeFs.default.mkdirSync(_nodePath.default.dirname(entryPath), {
|
|
187
|
-
recursive: true
|
|
188
|
-
});
|
|
189
|
-
_nodeFs.default.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n');
|
|
190
|
-
return entryPath;
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
exports.writeDocEntries = writeDocEntries;
|
|
194
|
-
const projectName = () => {
|
|
195
|
-
try {
|
|
196
|
-
return JSON.parse(_nodeFs.default.readFileSync(_nodePath.default.join(process.cwd(), 'package.json'), 'utf8')).name;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
return _nodePath.default.basename(process.cwd());
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
150
|
+
*/
|
|
151
|
+
exports.describeFile = describeFile;
|
|
152
|
+
const writeDocEntries = (ts, srcDir, entryDir) => {
|
|
153
|
+
const absoluteSrc = _nodePath.default.resolve(srcDir);
|
|
154
|
+
_nodeFs.default.mkdirSync(entryDir, {
|
|
155
|
+
recursive: true
|
|
156
|
+
});
|
|
157
|
+
const folders = listFolders(absoluteSrc).filter(folder => folder.files.some(file => !/^index\./.test(_nodePath.default.basename(file))));
|
|
158
|
+
const rootFiles = listSources(absoluteSrc).filter(file => !/^(index|main|browser)\./.test(_nodePath.default.basename(file)));
|
|
159
|
+
// Folders make modules, and so do the files which sit beside them (except the barrels: index, main and browser, the
|
|
160
|
+
// browser bundle's entry). A source directory with no folders gets a single module called index which holds its files.
|
|
161
|
+
const groups = folders.length ? [...folders, ...rootFiles.map(file => ({
|
|
162
|
+
name: toIdentifier(_nodePath.default.basename(file).replace(/\.[cm]?tsx?$/, '')),
|
|
163
|
+
files: [file],
|
|
164
|
+
description: describeFile(file)
|
|
165
|
+
})).filter(group => !folders.some(folder => folder.name === group.name))] : [{
|
|
166
|
+
name: 'index',
|
|
167
|
+
path: absoluteSrc,
|
|
168
|
+
files: rootFiles
|
|
169
|
+
}];
|
|
170
|
+
return groups.map(group => {
|
|
171
|
+
const lines = group.files.filter(file => !/^index\./.test(_nodePath.default.basename(file))).sort().flatMap(file => {
|
|
172
|
+
const modulePath = toModulePath(file);
|
|
173
|
+
const {
|
|
174
|
+
hasDefault,
|
|
175
|
+
hasNamed
|
|
176
|
+
} = describeExports(ts, file);
|
|
177
|
+
const fileName = _nodePath.default.basename(file).replace(/\.[cm]?tsx?$/, '');
|
|
178
|
+
return [...(hasDefault ? [`export { default as ${toIdentifier(fileName)} } from '${modulePath}'`] : []), ...(hasNamed ? [`export * from '${modulePath}'`] : [])];
|
|
179
|
+
});
|
|
180
|
+
const description = group.description ?? (group.path ? describeFolder(group.path) : '');
|
|
181
|
+
// TypeDoc takes the first comment of an entry file which has the @module tag as the description of the module
|
|
182
|
+
// A nested folder's module is named by its path (utilities/parsers), so it is not mistaken for its parent's
|
|
183
|
+
const moduleName = group.name.includes('/') ? ` ${group.name}` : '';
|
|
184
|
+
const moduleComment = description || moduleName ? `/**\n${description ? description.split('\n').map(line => ` * ${line}`.trimEnd()).join('\n') + '\n' : ''} * @module${moduleName}\n */\n\n` : '';
|
|
185
|
+
const entryPath = _nodePath.default.join(entryDir, `${group.name}.ts`);
|
|
186
|
+
_nodeFs.default.mkdirSync(_nodePath.default.dirname(entryPath), {
|
|
187
|
+
recursive: true
|
|
188
|
+
});
|
|
189
|
+
_nodeFs.default.writeFileSync(entryPath, moduleComment + lines.join('\n') + '\n');
|
|
190
|
+
return entryPath;
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
exports.writeDocEntries = writeDocEntries;
|
|
194
|
+
const projectName = () => {
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(_nodeFs.default.readFileSync(_nodePath.default.join(process.cwd(), 'package.json'), 'utf8')).name;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return _nodePath.default.basename(process.cwd());
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
202
|
/**
|
|
203
203
|
* Generate the HTML documentation of a TypeScript project with TypeDoc, straight from the TypeScript source, so the
|
|
204
204
|
* types (and the comments on them) are documented from where they are defined. The modules mirror the folders of the
|
|
@@ -209,51 +209,57 @@ const projectName = () => {
|
|
|
209
209
|
* @param {string} [outPath=gulpConfig.get('docs.to')] - The directory the documentation is generated into (cleared first).
|
|
210
210
|
* @param {function(string): Promise<Object>} [loadDocs] - Resolves the TypeDoc module, tests can inject a stand-in.
|
|
211
211
|
* @returns {Function} A gulp task, it accepts an optional callback and returns a promise.
|
|
212
|
-
*/
|
|
213
|
-
const typeDocsFor = (srcPath = gulpConfig.get('docs.from'), outPath = gulpConfig.get('docs.to'), loadDocs = loadTypeDoc) => {
|
|
214
|
-
if (gulpConfig.get('docs.enabled') === false) {
|
|
215
|
-
return () => {};
|
|
216
|
-
}
|
|
217
|
-
return done => {
|
|
218
|
-
const generate = async () => {
|
|
219
|
-
const ts = (0, _tsFor.loadTypescript)();
|
|
220
|
-
const {
|
|
221
|
-
Application
|
|
222
|
-
} = await loadDocs();
|
|
223
|
-
const workDir = _nodeFs.default.mkdtempSync(_nodePath.default.join(_nodeOs.default.tmpdir(), 'typedocs-'));
|
|
224
|
-
try {
|
|
225
|
-
const entryPoints = writeDocEntries(ts, srcPath, _nodePath.default.join(workDir, 'entries'));
|
|
226
|
-
// The entry files live outside the project, so the tsconfig which is used for the documentation extends the
|
|
227
|
-
// one of the project and adds the entries to the files it includes.
|
|
228
|
-
const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config');
|
|
229
|
-
const tsconfig = _nodePath.default.join(workDir, 'tsconfig.json');
|
|
230
|
-
_nodeFs.default.writeFileSync(tsconfig, JSON.stringify({
|
|
231
|
-
...(projectConfig ? {
|
|
232
|
-
extends: _nodePath.default.resolve(projectConfig)
|
|
233
|
-
} : {}),
|
|
234
|
-
include: [_nodePath.default.join(workDir, 'entries', '**', '*.ts').split(_nodePath.default.sep).join('/'), _nodePath.default.resolve(srcPath).split(_nodePath.default.sep).join('/') + '/**/*.ts'],
|
|
235
|
-
exclude: [_nodePath.default.resolve(srcPath).split(_nodePath.default.sep).join('/') + '/**/*.test.*']
|
|
236
|
-
}));
|
|
237
|
-
const indexFile = gulpConfig.get('docs.index');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
212
|
+
*/
|
|
213
|
+
const typeDocsFor = (srcPath = gulpConfig.get('docs.from'), outPath = gulpConfig.get('docs.to'), loadDocs = loadTypeDoc) => {
|
|
214
|
+
if (gulpConfig.get('docs.enabled') === false) {
|
|
215
|
+
return () => {};
|
|
216
|
+
}
|
|
217
|
+
return done => {
|
|
218
|
+
const generate = async () => {
|
|
219
|
+
const ts = (0, _tsFor.loadTypescript)();
|
|
220
|
+
const {
|
|
221
|
+
Application
|
|
222
|
+
} = await loadDocs();
|
|
223
|
+
const workDir = _nodeFs.default.mkdtempSync(_nodePath.default.join(_nodeOs.default.tmpdir(), 'typedocs-'));
|
|
224
|
+
try {
|
|
225
|
+
const entryPoints = writeDocEntries(ts, srcPath, _nodePath.default.join(workDir, 'entries'));
|
|
226
|
+
// The entry files live outside the project, so the tsconfig which is used for the documentation extends the
|
|
227
|
+
// one of the project and adds the entries to the files it includes.
|
|
228
|
+
const projectConfig = gulpConfig.get('docs.tsconfig') || gulpConfig.get('typescript.config');
|
|
229
|
+
const tsconfig = _nodePath.default.join(workDir, 'tsconfig.json');
|
|
230
|
+
_nodeFs.default.writeFileSync(tsconfig, JSON.stringify({
|
|
231
|
+
...(projectConfig ? {
|
|
232
|
+
extends: _nodePath.default.resolve(projectConfig)
|
|
233
|
+
} : {}),
|
|
234
|
+
include: [_nodePath.default.join(workDir, 'entries', '**', '*.ts').split(_nodePath.default.sep).join('/'), _nodePath.default.resolve(srcPath).split(_nodePath.default.sep).join('/') + '/**/*.ts'],
|
|
235
|
+
exclude: [_nodePath.default.resolve(srcPath).split(_nodePath.default.sep).join('/') + '/**/*.test.*']
|
|
236
|
+
}));
|
|
237
|
+
const indexFile = gulpConfig.get('docs.index');
|
|
238
|
+
const gitRevision = gulpConfig.get('docs.gitRevision');
|
|
239
|
+
await (0, _removeDirectory.removeDirectory)(outPath);
|
|
240
|
+
const app = await Application.bootstrap({
|
|
241
|
+
entryPoints,
|
|
242
|
+
tsconfig,
|
|
243
|
+
name: gulpConfig.get('docs.title') || projectName(),
|
|
244
|
+
readme: indexFile && _nodeFs.default.existsSync(indexFile) ? indexFile : 'none',
|
|
245
|
+
logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
|
|
246
|
+
skipErrorChecking: true,
|
|
247
|
+
// Left out, TypeDoc links each declaration to its source at the current commit, so every commit changes
|
|
248
|
+
// every page; a branch name keeps the pages the same until the documentation itself changes.
|
|
249
|
+
...(gitRevision ? {
|
|
250
|
+
gitRevision
|
|
251
|
+
} : {})
|
|
252
|
+
});
|
|
253
|
+
const project = await app.convert();
|
|
254
|
+
if (!project) {
|
|
255
|
+
throw new Error('TypeDoc could not read the TypeScript source, see the errors above.');
|
|
256
|
+
}
|
|
257
|
+
await app.generateDocs(project, outPath);
|
|
258
|
+
} finally {
|
|
259
|
+
await (0, _removeDirectory.removeDirectory)(workDir);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return typeof done === 'function' ? generate().then(() => done(), done) : generate();
|
|
263
|
+
};
|
|
264
|
+
};
|
|
259
265
|
exports.typeDocsFor = typeDocsFor;
|
|
@@ -25,6 +25,34 @@ export const loadImagemin = (projectPath = process.cwd()) => {
|
|
|
25
25
|
return loaded && (loaded.__esModule || loaded.default) ? loaded.default : loaded
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Load the plugins `gulp-imagemin` exports by name (gifsicle, mozjpeg, optipng and svgo), each a promise of a plugin
|
|
30
|
+
* factory, for when they need options of their own (see images.jpegQuality). Resolved like {@link loadImagemin}.
|
|
31
|
+
* @memberOf module:partials
|
|
32
|
+
* @param {string} [projectPath=process.cwd()] - The project directory to resolve `gulp-imagemin` from.
|
|
33
|
+
* @returns {Object<string, Promise<Function>>} The plugin factories, by name.
|
|
34
|
+
*/
|
|
35
|
+
export const loadImageminPlugins = (projectPath = process.cwd()) => requirePeer('gulp-imagemin', {
|
|
36
|
+
task: 'images',
|
|
37
|
+
installHint,
|
|
38
|
+
esmOnlyMessage: `gulp-imagemin could not be loaded because it is ESM-only. ${installHint}`,
|
|
39
|
+
projectPath
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// The plugins imagemin runs when it isn't given any, in its own order
|
|
43
|
+
const defaultPluginNames = ['gifsicle', 'mozjpeg', 'optipng', 'svgo']
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The plugins imagemin uses by default, with mozjpeg set to the given JPEG quality instead of its own default (75).
|
|
47
|
+
* @memberOf module:partials
|
|
48
|
+
* @param {number} jpegQuality - The mozjpeg quality, 0 to 100.
|
|
49
|
+
* @param {Object<string, Promise<Function>>} [plugins=loadImageminPlugins()] - The plugin factories, by name.
|
|
50
|
+
* @returns {Array<Promise<Function>>} The plugins, in imagemin's default order.
|
|
51
|
+
*/
|
|
52
|
+
export const imageminPlugins = (jpegQuality, plugins = loadImageminPlugins()) => defaultPluginNames.map(
|
|
53
|
+
name => Promise.resolve(plugins[name]).then(makePlugin => name === 'mozjpeg' ? makePlugin({ quality: jpegQuality }) : makePlugin())
|
|
54
|
+
)
|
|
55
|
+
|
|
28
56
|
/**
|
|
29
57
|
* Move and optimize images into the browser directory.
|
|
30
58
|
* @memberOf module:partials
|
|
@@ -34,16 +62,21 @@ export const loadImagemin = (projectPath = process.cwd()) => {
|
|
|
34
62
|
*/
|
|
35
63
|
export const imagesFor = (imageSrc = gulpConfig.get('images.from'), imageDest = gulpConfig.get('images.to')) => {
|
|
36
64
|
const imagemin = loadImagemin()
|
|
65
|
+
const jpegQuality = gulpConfig.get('images.jpegQuality')
|
|
66
|
+
// Left unset, imagemin runs its default plugins with their own defaults
|
|
67
|
+
const plugins = jpegQuality ? imageminPlugins(jpegQuality) : undefined
|
|
37
68
|
// Gulp 5 reads files as utf8 text unless told otherwise, which corrupts binary images (and makes gulp-cache drop them).
|
|
38
69
|
return src(imageSrc, { encoding: false })
|
|
39
70
|
.pipe(
|
|
40
71
|
// Caching images that ran through imagemin
|
|
41
72
|
cache(
|
|
42
|
-
imagemin({
|
|
73
|
+
imagemin(plugins, {
|
|
43
74
|
interlaced: true,
|
|
44
75
|
silent: true,
|
|
45
76
|
verbose: false,
|
|
46
|
-
})
|
|
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' }
|
|
47
80
|
)
|
|
48
81
|
)
|
|
49
82
|
.pipe(dest(imageDest))
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process'
|
|
1
2
|
import fs from 'fs'
|
|
2
3
|
import * as setUp from '../test-helpers/setUp.mjs'
|
|
3
4
|
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
4
6
|
import zlib from 'zlib'
|
|
5
|
-
import { imagesFor, loadImagemin } from './imagesFor.mjs'
|
|
7
|
+
import { imageminPlugins, imagesFor, loadImagemin } from './imagesFor.mjs'
|
|
6
8
|
import { isolateGulpCache } from '../test-helpers/isolateGulpCache.mjs'
|
|
7
9
|
|
|
8
10
|
setUp.setDefaults('test-images-for')
|
|
@@ -158,6 +160,25 @@ const makePng = (size = 32) => {
|
|
|
158
160
|
])
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// A JPEG at quality 100, encoded from a generated image by the cjpeg binary the mozjpeg dependency installs
|
|
164
|
+
const makeJpeg = (size = 64) => {
|
|
165
|
+
const pixels = Buffer.alloc(size * size * 3)
|
|
166
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
167
|
+
pixels[i] = (i * 37 + Math.floor(i / (size * 3)) * 91) % 256
|
|
168
|
+
}
|
|
169
|
+
const ppm = Buffer.concat([Buffer.from(`P6\n${size} ${size}\n255\n`), pixels])
|
|
170
|
+
// mozjpeg's exports field hides its files, and Jest runs from the project root
|
|
171
|
+
const cjpeg = path.resolve('node_modules', 'mozjpeg', 'vendor', 'cjpeg')
|
|
172
|
+
return execFileSync(cjpeg, ['-quality', '100'], { input: ppm })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Run imagesFor over one file and resolve with the written output
|
|
176
|
+
const optimize = (file, to) => new Promise((resolve, reject) => {
|
|
177
|
+
imagesFor(file, to)
|
|
178
|
+
.on('finish', () => resolve(fs.readFileSync(`${to}/${path.basename(file)}`)))
|
|
179
|
+
.on('error', reject)
|
|
180
|
+
})
|
|
181
|
+
|
|
161
182
|
// The cache outlives a run, so clear it or a stale result from an older imagemin can pass
|
|
162
183
|
beforeEach(() => clearImageCache().then(setUp.beforeEach))
|
|
163
184
|
|
|
@@ -210,6 +231,43 @@ describe('imagesFor', () => {
|
|
|
210
231
|
})
|
|
211
232
|
})
|
|
212
233
|
|
|
234
|
+
describe('images.jpegQuality', () => {
|
|
235
|
+
test('compresses JPEGs to the configured quality, never reusing another quality\'s cached result', async () => {
|
|
236
|
+
expect.assertions(4)
|
|
237
|
+
const srcPath = gulpConfig.get('srcPath')
|
|
238
|
+
const jpegFile = `${srcPath}/photo.jpg`
|
|
239
|
+
fs.writeFileSync(jpegFile, makeJpeg())
|
|
240
|
+
const browserPath = gulpConfig.get('browser.to')
|
|
241
|
+
try {
|
|
242
|
+
const atDefault = await optimize(jpegFile, `${browserPath}/default`)
|
|
243
|
+
gulpConfig.set('images.jpegQuality', 95)
|
|
244
|
+
const at95 = await optimize(jpegFile, `${browserPath}/q95`)
|
|
245
|
+
// Both still JPEGs (start-of-image marker)
|
|
246
|
+
expect(atDefault.subarray(0, 2).equals(Buffer.from([0xff, 0xd8]))).toBeTruthy()
|
|
247
|
+
expect(at95.subarray(0, 2).equals(Buffer.from([0xff, 0xd8]))).toBeTruthy()
|
|
248
|
+
// Same input, so a shared cache entry would make these identical: 95 keeps more detail than mozjpeg's 75
|
|
249
|
+
expect(at95.equals(atDefault)).toBe(false)
|
|
250
|
+
expect(at95.length).toBeGreaterThan(atDefault.length)
|
|
251
|
+
} finally {
|
|
252
|
+
gulpConfig.set('images.jpegQuality', false)
|
|
253
|
+
}
|
|
254
|
+
}, 30000)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('imageminPlugins', () => {
|
|
258
|
+
test('builds imagemin\'s default plugins in order, passing the quality to mozjpeg only', async () => {
|
|
259
|
+
expect.assertions(2)
|
|
260
|
+
const calls = []
|
|
261
|
+
const factory = name => Promise.resolve((...args) => {
|
|
262
|
+
calls.push([name, ...args])
|
|
263
|
+
return name
|
|
264
|
+
})
|
|
265
|
+
const plugins = { gifsicle: factory('gifsicle'), mozjpeg: factory('mozjpeg'), optipng: factory('optipng'), svgo: factory('svgo') }
|
|
266
|
+
expect(await Promise.all(imageminPlugins(85, plugins))).toEqual(['gifsicle', 'mozjpeg', 'optipng', 'svgo'])
|
|
267
|
+
expect(calls).toEqual([['gifsicle'], ['mozjpeg', { quality: 85 }], ['optipng'], ['svgo']])
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
213
271
|
describe('loadImagemin', () => {
|
|
214
272
|
test('loads the gulp-imagemin plugin factory from the project directory', () => {
|
|
215
273
|
expect.assertions(1)
|
|
@@ -204,6 +204,7 @@ export const typeDocsFor = (
|
|
|
204
204
|
exclude: [path.resolve(srcPath).split(path.sep).join('/') + '/**/*.test.*']
|
|
205
205
|
}))
|
|
206
206
|
const indexFile = gulpConfig.get('docs.index')
|
|
207
|
+
const gitRevision = gulpConfig.get('docs.gitRevision')
|
|
207
208
|
await removeDirectory(outPath)
|
|
208
209
|
const app = await Application.bootstrap({
|
|
209
210
|
entryPoints,
|
|
@@ -211,7 +212,10 @@ export const typeDocsFor = (
|
|
|
211
212
|
name: gulpConfig.get('docs.title') || projectName(),
|
|
212
213
|
readme: indexFile && fs.existsSync(indexFile) ? indexFile : 'none',
|
|
213
214
|
logLevel: gulpConfig.get('docs.logLevel') || 'Warn',
|
|
214
|
-
skipErrorChecking: true
|
|
215
|
+
skipErrorChecking: true,
|
|
216
|
+
// Left out, TypeDoc links each declaration to its source at the current commit, so every commit changes
|
|
217
|
+
// every page; a branch name keeps the pages the same until the documentation itself changes.
|
|
218
|
+
...(gitRevision ? { gitRevision } : {})
|
|
215
219
|
})
|
|
216
220
|
const project = await app.convert()
|
|
217
221
|
if (!project) {
|
|
@@ -211,6 +211,17 @@ describe('typeDocsFor', () => {
|
|
|
211
211
|
expect(calls.options.readme).toBe('none')
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
+
test('passes docs.gitRevision on to TypeDoc, and leaves TypeDoc\'s own default when it is false', async () => {
|
|
215
|
+
gulpConfig.set('docs.enabled', true)
|
|
216
|
+
const calls = {}
|
|
217
|
+
await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
|
|
218
|
+
expect('gitRevision' in calls.options).toBe(false)
|
|
219
|
+
gulpConfig.set('docs.gitRevision', 'main')
|
|
220
|
+
await typeDocsFor(gulpConfig.get('srcPath'), `${gulpConfig.get('srcPath')}/../docs`, fakeTypeDoc(calls))()
|
|
221
|
+
expect(calls.options.gitRevision).toBe('main')
|
|
222
|
+
gulpConfig.set('docs.gitRevision', false)
|
|
223
|
+
})
|
|
224
|
+
|
|
214
225
|
test('reads the source with the tsconfig of the project, adding the entries to it', async () => {
|
|
215
226
|
gulpConfig.set('docs.enabled', true)
|
|
216
227
|
gulpConfig.set('typescript.config', 'tsconfig.json')
|
package/gulp.config.mjs
CHANGED
|
@@ -16,6 +16,10 @@ import { fileExists } from 'test-filesystem'
|
|
|
16
16
|
* A setting that may be true or false.
|
|
17
17
|
* @typedef {boolean} module:gulpConfig~BooleanSetting
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* A setting that may be flag 'false' or provide a number
|
|
21
|
+
* @typedef {false|number} module:gulpConfig~FlagNumberSetting
|
|
22
|
+
*/
|
|
19
23
|
/**
|
|
20
24
|
* A setting that may be flag 'false' or provide a StringSetting
|
|
21
25
|
* @typedef {false|StringSetting} module:gulpConfig~FlagStringSetting
|
|
@@ -43,7 +47,7 @@ import { fileExists } from 'test-filesystem'
|
|
|
43
47
|
*/
|
|
44
48
|
/**
|
|
45
49
|
* Any single configuration option is a Setting.
|
|
46
|
-
* @typedef {ArrayableSetting|BooleanSetting|FlagsSetting|StringSetting} Setting
|
|
50
|
+
* @typedef {ArrayableSetting|BooleanSetting|FlagNumberSetting|FlagsSetting|StringSetting} Setting
|
|
47
51
|
*/
|
|
48
52
|
/**
|
|
49
53
|
* One copy-files entry: a glob (or array of globs) to copy, and where to copy it.
|
|
@@ -103,6 +107,9 @@ import { fileExists } from 'test-filesystem'
|
|
|
103
107
|
* @typedef {Object<string, Setting>} module:gulpConfig~ImageConfig
|
|
104
108
|
* @property {BooleanSetting} enabled - Toggle image minify and copy process.
|
|
105
109
|
* @property {StringSetting} from - Path to search for images.
|
|
110
|
+
* @property {FlagNumberSetting} jpegQuality - The JPEG quality (0 to 100) mozjpeg compresses to. False (the
|
|
111
|
+
* default) keeps mozjpeg's own default, 75. Raise it (85 is usually indistinguishable from the original) when 75
|
|
112
|
+
* visibly bands smooth gradients.
|
|
106
113
|
* @property {StringSetting} to - Path to output images.
|
|
107
114
|
*/
|
|
108
115
|
/**
|
|
@@ -135,6 +142,10 @@ import { fileExists } from 'test-filesystem'
|
|
|
135
142
|
* build does this instead of generating the readme from the jsdoc comments of the compiled files.
|
|
136
143
|
* @property {ArrayableSetting} from - The directory holding the source, each folder in it becomes a module (TypeDoc) or
|
|
137
144
|
* is scanned recursively (jsdoc). jsdoc also takes a list of directories and files.
|
|
145
|
+
* @property {FlagStringSetting} gitRevision - The git revision TypeDoc's "Defined in" source links point to (TypeDoc
|
|
146
|
+
* only). False (the default) is TypeDoc's own: the current commit, which changes every page on every commit. A
|
|
147
|
+
* branch name such as 'main' keeps the pages unchanged until the documentation itself changes; '{branch}' is the
|
|
148
|
+
* branch being built.
|
|
138
149
|
* @property {BooleanSetting} inBuild - Generate the documentation as part of build (the default). Turn it off to only
|
|
139
150
|
* generate it when you run the docs task yourself (gulp docs), for example when docs.to points into another
|
|
140
151
|
* repository and you only want to publish the documentation of a release.
|
|
@@ -219,6 +230,7 @@ const setDefaults = {
|
|
|
219
230
|
docs: {
|
|
220
231
|
enabled: false,
|
|
221
232
|
from: 'src',
|
|
233
|
+
gitRevision: false,
|
|
222
234
|
inBuild: true,
|
|
223
235
|
index: 'MAIN.md',
|
|
224
236
|
logLevel: 'Warn',
|
|
@@ -236,6 +248,7 @@ const setDefaults = {
|
|
|
236
248
|
images: {
|
|
237
249
|
enabled: false,
|
|
238
250
|
from: 'src/img/**/*.+(png|jpg|jpeg|gif|svg)',
|
|
251
|
+
jpegQuality: false,
|
|
239
252
|
to: 'browser/img',
|
|
240
253
|
},
|
|
241
254
|
lint: {
|