@maddimathon/build-utilities
Version:
Opinionated utilities for easy build systems in npm projects.
796 lines (795 loc) • 26.4 kB
JavaScript
/**
* @since 0.1.0-alpha
*
* @packageDocumentation
*/
/*!
* @maddimathon/build-utilities@0.3.0-alpha.4
* @license MIT
*/
import { globSync } from 'glob';
// import { minify } from 'minify';
import * as prettier from 'prettier';
import {
escRegExp,
escRegExpReplace,
mergeArgs,
} from '@maddimathon/utility-typescript/functions';
import { node } from '@maddimathon/utility-typescript/classes';
import {
AbstractError,
isObjectEmpty,
logError,
} from '../../@internal/index.js';
/**
* Extends the {@link node.NodeFiles} class with some custom logic useful to this package.
*
* @category Utilities
*
* @since 0.1.0-alpha
*/
export class FileSystem extends node.NodeFiles {
/* STATIC
* ====================================================================== */
/**
* Default {@link prettier} configuration object.
*
* @category Args
*
* @since 0.2.0-alpha.2
*/
static get prettierConfig() {
/**
* @param format If defined, the override for the given format is
* merged into the object. If undefined, all
* overrides are formatted for a JSON config file.
*/
function toJSON(format) {
const overrides = this.overrides;
const json = {
...this,
overrides: [],
};
delete json.toJSON;
// returns
if (format) {
return mergeArgs(json, overrides[format], true);
}
for (const _t_format in overrides) {
const _format = _t_format;
// continues
if (!overrides[_format]) {
continue;
}
// continues
if (isObjectEmpty(overrides[_format])) {
continue;
}
switch (_format) {
case 'css':
case 'html':
case 'js':
case 'json':
case 'md':
case 'mdx':
case 'scss':
case 'ts':
json.overrides.push({
files: '*.' + _format,
options: overrides[_format],
});
break;
case 'yaml':
json.overrides.push({
files: ['*.yaml', '*.yml'],
options: overrides[_format],
});
break;
default:
true;
break;
}
}
return json;
}
const config = {
// checkIgnorePragma: false, // default
// insertPragma: false, // default
// objectWrap: 'preserve', // default
// rangeEnd: Number.POSITIVE_INFINITY, // default
// rangeStart: 0, // default
// requirePragma: false, // default
bracketSameLine: false, // default
bracketSpacing: true, // default
experimentalOperatorPosition: 'start',
experimentalTernaries: true,
htmlWhitespaceSensitivity: 'strict',
jsxSingleQuote: true, // default
printWidth: 80, // default
proseWrap: 'preserve', // default
semi: true, // default
singleAttributePerLine: true,
singleQuote: true,
tabWidth: 4,
trailingComma: 'all', // default
useTabs: false,
overrides: {
css: {
singleQuote: false,
},
html: {
printWidth: 10000,
singleQuote: false,
},
md: {
printWidth: 10000,
},
mdx: {
printWidth: 10000,
},
js: undefined,
json: undefined,
scss: undefined,
ts: undefined,
yaml: undefined,
},
toJSON,
};
config.toJSON = config.toJSON.bind(config);
config.valueOf = config.toJSON;
return config;
}
/* LOCAL PROPERTIES
* ====================================================================== */
/**
* Instance used to log messages within the class.
*
* @category Classes
*/
console;
/* Args ===================================== */
args;
get ARGS_DEFAULT() {
const copy = {
force: true,
recursive: true,
rename: true,
glob: {
absolute: false,
dot: true,
filesOnly: false,
},
};
const glob = {
absolute: true,
dot: true,
ignore: [...FileSystem.globs.SYSTEM],
};
const prettier = {
_: FileSystem.prettierConfig,
css: FileSystem.prettierConfig.overrides.css,
html: FileSystem.prettierConfig.overrides.html,
js: FileSystem.prettierConfig.overrides.js,
json: FileSystem.prettierConfig.overrides.json,
md: FileSystem.prettierConfig.overrides.md,
mdx: FileSystem.prettierConfig.overrides.mdx,
scss: FileSystem.prettierConfig.overrides.scss,
ts: FileSystem.prettierConfig.overrides.ts,
yaml: FileSystem.prettierConfig.overrides.yaml,
};
return {
...node.NodeFiles.prototype.ARGS_DEFAULT,
argsRecursive: true,
copy,
glob,
minify: FileSystem.minify.ARGS_DEFAULT,
prettier,
};
}
buildArgs(args) {
const merged = mergeArgs(this.ARGS_DEFAULT, args, true);
let prettier;
if (typeof merged.prettier === 'function') {
prettier = {
css: merged.prettier('css'),
html: merged.prettier('html'),
js: merged.prettier('js'),
json: merged.prettier('json'),
md: merged.prettier('md'),
mdx: merged.prettier('mdx'),
scss: merged.prettier('scss'),
ts: merged.prettier('ts'),
yaml: merged.prettier('yaml'),
};
} else if (!('_' in merged.prettier) || !merged.prettier._) {
const _mergedPrettier = merged.prettier;
prettier = {
css: _mergedPrettier,
html: _mergedPrettier,
js: _mergedPrettier,
json: _mergedPrettier,
md: _mergedPrettier,
mdx: _mergedPrettier,
scss: _mergedPrettier,
ts: _mergedPrettier,
yaml: _mergedPrettier,
};
} else {
const _mergedPrettier = merged.prettier;
prettier = {
css: mergeArgs(_mergedPrettier._, _mergedPrettier.css, true),
html: mergeArgs(_mergedPrettier._, _mergedPrettier.html, true),
js: mergeArgs(_mergedPrettier._, _mergedPrettier.js, true),
json: mergeArgs(_mergedPrettier._, _mergedPrettier.json, true),
md: mergeArgs(_mergedPrettier._, _mergedPrettier.md, true),
mdx: mergeArgs(_mergedPrettier._, _mergedPrettier.mdx, true),
scss: mergeArgs(_mergedPrettier._, _mergedPrettier.scss, true),
ts: mergeArgs(_mergedPrettier._, _mergedPrettier.ts, true),
yaml: mergeArgs(_mergedPrettier._, _mergedPrettier.yaml, true),
};
}
return {
...merged,
prettier,
};
}
/* CONSTRUCTOR
* ====================================================================== */
/**
* @category Constructor
*
* @param console Instance used to log messages and debugging info.
* @param args Override arguments.
*/
constructor(console, args = {}) {
super(args, {
nc: console.nc,
});
this.console = console;
this.args =
// @ts-expect-error - it is initialized in the super constructor
this.args ?? this.buildArgs(args);
}
/* METHODS
* ====================================================================== */
/**
* {@inheritDoc internal.FileSystemType.copy}
*
* @category Filers
*
* @throws {@link FileSystem.Error} — If copying a file fails.
*/
copy(globs, level, outputDir, sourceDir, args) {
args = mergeArgs(this.args.copy, args, true);
outputDir = './' + outputDir.replace(/(^\.\/|\/$)/g, '') + '/';
if (sourceDir) {
sourceDir = './' + sourceDir.replace(/(^\.\/|\/$)/g, '') + '/';
}
if (!Array.isArray(globs)) {
globs = [globs];
}
const copyPaths = this.glob(
sourceDir ?
globs.map((glob) => this.pathResolve(sourceDir, glob))
: globs,
args.glob,
);
const sourceDirRegex =
sourceDir
&& new RegExp(
'^' + escRegExp(this.pathRelative(sourceDir) + '/'),
'gi',
);
const output = [];
for (const source of copyPaths) {
const source_relative = this.pathRelative(source);
const destination = this.pathResolve(
outputDir,
sourceDirRegex ?
source_relative.replace(sourceDirRegex, '')
: source_relative,
);
this.console.debug(
`(TESTING) ${source_relative} → ${this.pathRelative(destination)}`,
level,
{ linesIn: 0, linesOut: 0, maxWidth: null },
);
const t_output = this.copyFile(source, destination, args);
// throws
if (!t_output) {
throw new FileSystem.Error(
[
'this.copyFile returned falsey',
'source = ' + source,
'destination = ' + this.pathRelative(destination),
].join('\n'),
'copy',
);
}
output.push(t_output);
}
return output;
}
/**
* Deletes given globs (via {@link node.NodeFiles}.delete).
*
* This catches any errors from {@link node.NodeFiles}.delete, ignores
* ENOTEMPTY errors, and re-throws the rest.
*
* @category Filers
*
* @param globs Glob patterns for paths to delete.
* @param level Depth level for output to the console.
* @param dryRun If true, files that would be deleted are printed to the
* console and not deleted.
* @param args Optional glob configuration.
*/
delete(globs, level, dryRun, args) {
try {
return super.delete(this.glob(globs, args), level, dryRun);
} catch (error) {
if (
error
&& typeof error === 'object'
&& 'message' in error
&& String(error.message)?.match(/^\s*ENOTEMPTY\b/g)
) {
logError(
'Error (ENOTEMPTY) caught and ignored during FileSystem.delete()',
error,
level,
{
console: this.console,
fs: this,
},
);
} else {
throw error;
}
}
}
/**
* {@inheritDoc internal.FileSystemType.glob}
*
* @category Path-makers
*/
glob(globs, args = {}) {
if (!Array.isArray(globs)) {
globs = [globs];
}
args = mergeArgs(this.args.glob, args, false);
const globResult = globSync(globs, args)
.map((res) => (typeof res === 'object' ? res.fullpath() : res))
.filter((_path) => _path.match(/(^|\/)\._/g) === null);
if (args.filesOnly) {
return globResult.filter(this.isFile);
}
return globResult;
}
/**
* {@inheritDoc internal.FileSystemType.minify}
*
* @category Transformers
*/
async minify(globs, format, level, args, renamer) {
args = mergeArgs(
typeof this.args.minify === 'function' ?
this.args.minify(format)
: this.args.minify,
args ?? {},
true,
);
let minimizerFn;
// returns if no match for minimizer function
switch (format) {
case 'css':
case 'html':
case 'js':
// minimizerFn = async ( _p ) => minify[ format ]( _p.content, args[ format ] );
// minimizerFn = async ( _p ) => minify( _p.path, args );
this.console.log(
`minimizing for ${format} is not yet implemented`,
level,
{ italic: true },
);
return [];
case 'json':
minimizerFn = (_p) =>
JSON.stringify(JSON.parse(_p.content), null, 0);
break;
default:
this.console.warn(
[[`minimizing for ${format} is not yet supported`]],
level,
{ italic: true },
);
return [];
}
const files = this.glob(globs, args.glob).filter(
(_inputPath) => this.exists(_inputPath) && this.isFile(_inputPath),
);
// returns
if (!files.length) {
return [];
}
const minimized = [];
for (const _inputPath of files) {
let _content = this.readFile(_inputPath);
// continues
if (!_content) {
continue;
}
this.console.params.debug
&& this.console.verbose(
'minimizing ' + this.pathRelative(_inputPath) + ' ...',
level,
{ maxWidth: null },
);
try {
_content = await minimizerFn({
path: _inputPath,
content: _content,
});
} catch (_error) {
// throws
if (!(_error instanceof TypeError)) {
throw _error;
}
logError(
'TypeError caught and ignored during FileSystem.minify() with '
+ this.pathRelative(_inputPath),
_error,
1 + level,
{
console: this.console,
fs: this,
},
);
}
if (_content) {
const _outputPath = renamer?.(_inputPath) ?? _inputPath;
if (
this.write(_outputPath, _content, {
force: true,
rename: false,
})
) {
minimized.push({
source: _inputPath,
output: _outputPath,
});
}
}
}
return minimized;
}
/**
* {@inheritDoc internal.FileSystemType.prettier}
*
* @category Transformers
*
* @throws {@link FileSystem.Error} — If no parser was given or could be
* automatically assigned based on the format (this is unlikely if
* you respect the {@link FileSystemType.Prettier.Format} type).
*/
async prettier(globs, format, args) {
args = mergeArgs(this.args.prettier[format], args ?? {}, true);
// throws if no parser can be set
if (!args.parser) {
switch (format) {
case 'css':
case 'html':
case 'mdx':
case 'json':
case 'scss':
case 'yaml':
args.parser = format;
break;
case 'js':
args.parser = 'babel';
break;
case 'md':
args.parser = 'markdown';
break;
case 'ts':
args.parser = 'typescript';
break;
default:
throw new FileSystem.Error(
'No parser was given or assigned for format "'
+ format
+ '"',
'prettify',
);
return [];
}
}
const files = this.glob(globs, args.glob).filter(
(_path) => this.exists(_path) && this.isFile(_path),
);
// returns
if (!files.length) {
return [];
}
const prettified = [];
for (const _path of files) {
let _content = this.readFile(_path);
// continues
if (!_content) {
continue;
}
_content = await prettier.format(_content, args);
if (_content) {
if (
this.write(_path, _content, { force: true, rename: false })
) {
prettified.push(_path);
}
}
}
return prettified;
}
/**
* {@inheritDoc internal.FileSystemType.replaceInFiles}
*
* @category Transformers
*/
replaceInFiles(globs, replace, level, args) {
// returns
if (!replace.length) {
this.console.verbose(
'FileSystem.replaceInFiles() - no replacements passed',
level,
);
this.console.vi.debug(
{ replace },
(this.console.params.verbose ? 1 : 0) + level,
);
return [];
}
const replacements = (
Array.isArray(replace[0]) ? replace : [replace]).filter(
([find, repl]) => find && typeof repl !== 'undefined',
);
// returns
if (!replacements.length) {
this.console.verbose(
'FileSystem.replaceInFiles() - no valid replacement args passed',
level,
);
this.console.vi.debug(
{ replace },
(this.console.params.verbose ? 1 : 0) + level,
);
this.console.vi.debug(
{ replacements },
(this.console.params.verbose ? 1 : 0) + level,
);
return [];
}
const files = this.glob(globs, args).filter((path) => {
const _stats = this.getStats(path);
// returns
if (!_stats || !_stats.isFile() || _stats.isSymbolicLink()) {
return false;
}
return true;
});
// returns
if (!files.length) {
this.console.vi.debug(
{ globs },
(this.console.params.verbose ? 1 : 0) + level,
);
return [];
}
if (this.console.params.debug && this.console.params.verbose) {
const _level = (this.console.params.verbose ? 1 : 0) + level;
let i = 1;
for (const [find, repl] of replacements) {
this.console.verbose('set of replacements #' + i, _level);
this.console.vi.verbose({ find }, 1 + _level);
this.console.vi.verbose({ repl }, 1 + _level);
i++;
}
}
const replaced = [];
for (const _path of files) {
let _content = this.readFile(_path);
let _write = false;
// continues
if (!_content) {
continue;
}
for (const [find, repl] of replacements) {
const _regex =
find instanceof RegExp ? find : (
new RegExp(escRegExp(find), 'g')
);
if (!!_content.match(_regex)) {
_content = _content.replace(
_regex,
escRegExpReplace(String(repl)),
);
_write = true;
}
}
if (_write) {
if (
this.write(_path, _content, { force: true, rename: false })
) {
replaced.push(_path);
}
}
}
return replaced;
}
}
/**
* Used only for {@link FileSystem}.
*
* @category Utilities
*
* @since 0.1.0-alpha
*/
(function (FileSystem) {
/**
* An extension of the utilities error used by the {@link FileSystem} class.
*
* @category Errors
*
* @since 0.1.0-alpha
*/
class Error extends AbstractError {
name = 'FileSystem Error';
constructor(message, method, args) {
super(message, { class: 'FileSystem', method }, args);
}
}
FileSystem.Error = Error;
/**
* Arrays of utility globs used within the library.
*
* @category Utilities
*
* @since 0.1.0-alpha
*/
let globs;
(function (globs) {
/**
* Files that are copied into subdirectories (e.g., releases and
* snapshots).
*
* @since 0.1.0-alpha
*
* @source
*/
globs.IGNORE_COPIED = (stage) => [
stage.config.paths.release.replace(/\/$/g, '') + '/**',
stage.config.paths.snapshot.replace(/\/$/g, '') + '/**',
];
/**
* Compiled files to ignore.
*
* @since 0.1.0-alpha
*
* @source
*/
globs.IGNORE_COMPILED = (stage) => [
stage.getDistDir().replace(/\/$/g, '') + '/**',
stage.getDistDir('docs').replace(/\/$/g, '') + '/**',
stage.getDistDir('scss').replace(/\/$/g, '') + '/**',
];
/**
* Files that we probably want to ignore within an npm project.
*
* @since 0.1.0-alpha
*/
globs.IGNORE_PROJECT = [
'.git/**',
'**/.git/**',
'.scripts/**',
'**/.scripts/**',
'.vscode/**/*.code-snippets',
'.vscode/**/settings.json',
'node_modules/**',
'**/node_modules/**',
];
/**
* System files that we *never, ever* want to include.
*
* @since 0.1.0-alpha
*/
globs.SYSTEM = [
'._*',
'._*/**',
'**/._*',
'**/._*/**',
'**/.DS_Store',
'**/.smbdelete**',
];
})((globs = FileSystem.globs || (FileSystem.globs = {})));
/**
* Utility functions and constants for the {@link FileSystem.minify} method.
*
* @category Transformers
*
* @since 0.1.0-alpha
*/
let minify;
(function (minify) {
/**
* Default args for the {@link FileSystem.minify} method
*
* @since 0.1.0-alpha
*/
minify.ARGS_DEFAULT = {
css: {
// type: "clean-css",
// 'clean-css': {
// compatibility: "*",
// },
},
html: {
// collapseBooleanAttributes: false,
// collapseWhitespace: true,
// minifyCSS: true,
// minifyJS: true,
// removeAttributeQuotes: true,
// removeCDATASectionsFromCDATA: true,
// removeComments: true,
// removeCommentsFromCDATA: true,
// removeEmptyAttributes: false,
// removeEmptyElements: false,
// removeOptionalTags: false,
// removeRedundantAttributes: false,
// removeScriptTypeAttributes: false,
// removeStyleLinkTypeAttributes: false,
// useShortDoctype: true,
},
js: {
// type: 'putout',
// putout: {
// quote: "'",
// mangle: false,
// mangleClassNames: false,
// removeUnusedVariables: false,
// removeConsole: false,
// removeUselessSpread: false,
// },
},
json: {},
glob: {},
};
})((minify = FileSystem.minify || (FileSystem.minify = {})));
/**
* Utility functions for the {@link FileSystem.prettier} method.
*
* @category Transformers
*
* @since 0.1.0-alpha
*/
let prettier;
(function (prettier) {
/**
* Default args for the {@link FileSystem.prettier} method
*
* @since 0.1.0-alpha
*
* @deprecated 0.2.0-alpha.2 — Replaced by static accessor {@link FileSystem.prettierConfig}.
*/
prettier.ARGS_DEFAULT = {
_: FileSystem.prettierConfig,
css: FileSystem.prettierConfig.overrides.css,
html: FileSystem.prettierConfig.overrides.html,
js: FileSystem.prettierConfig.overrides.js,
json: FileSystem.prettierConfig.overrides.json,
md: FileSystem.prettierConfig.overrides.md,
mdx: FileSystem.prettierConfig.overrides.mdx,
scss: FileSystem.prettierConfig.overrides.scss,
ts: FileSystem.prettierConfig.overrides.ts,
yaml: FileSystem.prettierConfig.overrides.yaml,
};
})((prettier = FileSystem.prettier || (FileSystem.prettier = {})));
})(FileSystem || (FileSystem = {}));
//# sourceMappingURL=FileSystem.js.map