@nasriya/postbuild
Version:
A package that does some tasks after compilation
391 lines (352 loc) • 18.9 kB
JavaScript
import path from 'path';
import fs from 'fs';
import utils from './utils/utils.js';
import constants from './constants.js';
class Main {
#_verbose = false;
#_configFile = {}
#_config = {}
#_helpers = {
print: (str, force) => {
if (this.#_verbose || force === true) {
console.info(str);
}
},
/**Reads configurations from the file */
read: () => {
this.#_verbose = this.#_configFile?.verbose === true;
if (utils.is.undefined(this.#_configFile) || !utils.is.realObject(this.#_configFile)) { throw new Error(`The ${constants.PACKAGE_NAME}'s "${constants.CONFIG_FILE_NAME}" is either missing or invalid`) }
this.#_helpers.print('Reading config file...');
try {
if ('esmDir' in this.#_configFile) {
if (this.#_configFile.esmDir === 'auto') {
this.#_config.esmDir = path.join(constants.ROOT, constants.ESM_REL_PATH);
} else {
const fullPath = path.resolve(this.#_configFile.esmDir)
if (fs.existsSync(fullPath)) {
this.#_config.esmDir = fullPath;
} else {
throw new Error(`The provided "esmDir":"${fullPath}" does not exist`);
}
}
}
if ('cjsDir' in this.#_configFile) {
if (this.#_configFile.cjsDir === 'auto') {
this.#_config.cjsDir = path.join(constants.ROOT, constants.CJS_REL_PATH)
} else {
const fullPath = path.resolve(this.#_configFile.cjsDir)
if (fs.existsSync(fullPath)) {
this.#_config.cjsDir = fullPath;
} else {
throw new Error(`The provided "cjsDir":"${fullPath}" does not exist`);
}
}
}
if ('copyFiles' in this.#_configFile) {
if (utils.is.undefined(this.#_configFile.copyFiles) || !utils.is.realObject(this.#_configFile.copyFiles)) { throw new Error(`The provided "copyFiles" must be a real object`) }
this.#_config.copyFiles = { exclude: [], from: '' };
if ('exclude' in this.#_configFile.copyFiles) {
if (Array.isArray(this.#_configFile.copyFiles.exclude)) {
for (const inc of this.#_configFile.copyFiles.exclude) {
if (utils.is.validString(inc)) {
if (!inc.startsWith('.')) { throw new Error(`The "copyFiles.exclude" should include files extensions in this format: ".ts". ${inc} is not a valid extension format.`) }
continue;
} else {
throw new Error(`The "copyFiles.exclude" contains an invalid directory (${inc}). Expecting a string but instead got ${typeof inc}`)
}
}
this.#_config.copyFiles.exclude = ['.ts'].concat(this.#_configFile.copyFiles.exclude);
} else {
throw new Error(`The "copyFiles.exclude" is expecting an array of strings, instead got ${typeof this.#_configFile.copyFiles.exclude}`)
}
} else {
this.#_configFile.copyFiles.exclude = ['.ts'];
}
if ('from' in this.#_configFile.copyFiles) {
if (!utils.is.validString(this.#_configFile.copyFiles.from)) { throw new Error(`The "copyFiles.from" expecting a string but instead got ${typeof this.#_configFile.copyFiles.from}`) }
if (!fs.existsSync(path.resolve(this.#_configFile.copyFiles.from))) { throw new Error(`The "copyFiles.from" directory (${this.#_configFile.copyFiles.from}) does not exist`) }
this.#_config.copyFiles.from = path.resolve(this.#_configFile.copyFiles.from);
} else {
this.#_config.copyFiles.from = path.resolve('./src');
}
}
if ('addExtensions' in this.#_configFile) {
if (typeof this.#_configFile.addExtensions !== 'boolean') { throw new Error(`The "addExtensions" option is expecting a boolean value, instead got ${typeof this.#_configFile.addExtensions}`) }
this.#_config.addExtensions = this.#_configFile.addExtensions;
} else {
this.#_config.addExtensions = false;
}
if ('aliases' in this.#_configFile) {
const aliases = this.#_configFile.aliases;
if (!utils.is.realObject(aliases)) { throw new Error(`The "aliases" option is expecting a real object, isntead got ${typeof aliases}`) }
this.#_config.aliases = {}
for (const prop in aliases) {
if (typeof aliases[prop] === 'string') {
this.#_config.aliases[prop] = aliases[prop];
} else {
throw new TypeError(`One of the defined aliases (${aliases[prop]}) is not a string`)
}
}
}
} catch (error) {
if (error instanceof Error) {
error.message = `Unable to read postbuild configFile: ${error.message}`;
}
throw error;
}
},
copy: {
exclude: ['.ts'],
folderContent: (from, to, exclude) => {
if (!fs.existsSync(to)) { fs.mkdirSync(to, { recursive: true }) }
if (path.basename(from).toLowerCase().includes('logs')) { return }
const files = fs.readdirSync(from, { withFileTypes: true });
for (const file of files) {
if (file.isFile()) {
const filePath = path.join(file.path, file.name);
if (path.extname(filePath) === '.ts') { continue }
fs.copyFileSync(filePath, path.join(to, file.name));
this.#_helpers.print(`Copied: ${filePath}`);
} else {
this.#_helpers.copy.folderContent(path.join(from, file.name), path.join(to, file.name), exclude)
}
}
},
directory: (src, dest, exclude) => {
this.#_helpers.copy.folderContent(src, dest, exclude);
},
run: () => {
if (this.#_config.copyFiles) {
const from = this.#_config.copyFiles.from;
const exclude = this.#_config.copyFiles.exclude;
if (from) {
if (this.#_config.esmDir) {
this.#_helpers.print('Copying files to ESM...');
this.#_helpers.copy.directory(from, path.join(constants.ROOT, constants.ESM_REL_PATH), exclude);
this.#_helpers.addJsonImportAssertions(this.#_config.esmDir);
this.#_helpers.convertDirToImport(this.#_config.esmDir);
}
if (this.#_config.cjsDir) {
this.#_helpers.print('Copying files to CJS...');
this.#_helpers.copy.directory(from, path.join(constants.ROOT, constants.CJS_REL_PATH), exclude);
}
}
}
}
},
extensions: {
add: (dir) => {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.lstatSync(fullPath).isDirectory()) {
this.#_helpers.extensions.add(fullPath);
} else if (file.endsWith('.js')) {
let content = fs.readFileSync(fullPath, 'utf8');
// Regex to match import statements with relative paths
content = content.replace(/import\s+(?:[\w{}\s,*]+)?\s*(?:\w+)?(?:\s+from\s+)?['"](\.\/.*?|\.\.\/.*?)(['"])/g, (match, p1, p2, p3) => {
// Only modify relative paths that don't already end with '.js'
if (!p1.endsWith('.js') && (p1.startsWith('./') || p1.startsWith('../'))) {
// Construct the new import statement
const start = match.substring(0, match.indexOf(p1));
return `${start}${p1}.js${p2}`;
}
return match;
});
// Regex to match export statements
content = content.replace(/(export\s+\{\s*.*\s*}\s+from\s+['"])(.*)(['"];)/g, (match, p1, p2, p3) => {
if (!p2.endsWith('.js') && !p2.startsWith('.') && !p2.startsWith('/')) {
return match; // Skip non-relative exports
}
const lastPart = p2.split('/').pop();
if (lastPart.includes('.') && lastPart.split('.').length > 1) {
return match; // Skip if last part already has an extension
}
return `${p1}${p2}.js${p3}`;
});
fs.writeFileSync(fullPath, content, 'utf8');
}
});
},
run: () => {
if (this.#_config.addExtensions === true) {
if (this.#_config.esmDir) {
this.#_helpers.print('Adding extensions to ESM...');
this.#_helpers.extensions.add(this.#_config.esmDir);
}
if (this.#_config.cjsDir) {
this.#_helpers.print('Adding extensions to CJS...');
this.#_helpers.extensions.add(this.#_config.cjsDir);
}
}
}
},
create: {
pkgESM: () => {
this.#_helpers.print('Creating package.json for "ESM"...');
const content = JSON.stringify({
"type": "module"
}, null, 4);
this.#_helpers.create.write(this.#_config.esmDir, content);
},
pkgCJS: () => {
this.#_helpers.print('Creating package.json for "CJS"...');
const content = JSON.stringify({
"type": "commonjs"
}, null, 4);
this.#_helpers.create.write(this.#_config.cjsDir, content);
},
write: (dir, content) => {
fs.writeFileSync(path.join(dir, 'package.json'), content, { encoding: 'utf-8' });
},
packages: () => {
if (this.#_config.esmDir) {
this.#_helpers.create.pkgESM();
}
if (this.#_config.cjsDir) {
this.#_helpers.create.pkgCJS();
}
}
},
config: {
check: () => {
const configPath = path.resolve(constants.CONFIG_FILE_NAME);
if (!fs.existsSync(configPath)) { throw new Error(`The ${constants.PACKAGE_NAME}'s config file is missing.`) }
const str_configs = fs.readFileSync(configPath, { encoding: 'utf-8' });
try {
this.#_configFile = JSON.parse(str_configs);
} catch (error) {
throw new Error(`The ${constants.PACKAGE_NAME}'s config file is not a valid JSON file.`)
}
}
},
addJsonImportAssertions: (dir) => {
this.#_helpers.print('Add JSON import assertion');
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.lstatSync(fullPath).isDirectory()) {
this.#_helpers.addJsonImportAssertions(fullPath);
} else if (file.endsWith('.js') || file.endsWith('.mjs')) {
let content = fs.readFileSync(fullPath, 'utf8');
content = content.replace(/import\s+(.*?)\s+from\s+(['"])(.*?)\.json\2\s*;/g, (match, imports, quote, modulePath) => {
if (!imports.includes('assert')) {
return `import ${imports} from ${quote}${modulePath}.json${quote} assert { type: "json" };`;
}
return match;
});
fs.writeFileSync(fullPath, content, 'utf8');
}
});
},
/**This method converts `_dirname = __dirname` to `_dirname = import.meta.dirname` */
convertDirToImport: (dir) => {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.lstatSync(fullPath).isDirectory()) {
this.#_helpers.convertDirToImport(fullPath);
} else if (file.endsWith('.js')) {
let content = fs.readFileSync(fullPath, 'utf8');
// Replace __dirname with import.meta.dirname
content = content.replace(/__dirname/g, 'import.meta.dirname');
fs.writeFileSync(fullPath, content, 'utf8');
}
});
},
aliases: {
regex: {
/**
* Create a regular expression for catching imports
* @param {string} pattern
* @returns {RegExp}
*/
createCatch: (pattern) => {
const escapedPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^${escapedPattern}$`)
},
/**
* Create a regular expression for catching imports
* @param {string} pattern
* @returns {RegExp}
*/
createExact: (pattern) => {
const escapedPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&');
return new RegExp(`^${escapedPattern}`)
}
},
getPatterns: () => {
const aliases = this.#_config.aliases;
return Object.keys(aliases).map(alias => {
const catchPattern = this.#_helpers.aliases.regex.createCatch(alias);
const exactPattern = this.#_helpers.aliases.regex.createExact(alias);
return {
alias,
catchPattern,
exactPattern,
resolvedPath: aliases[alias]
};
});
},
matchImportPath: (importPath, aliasPatterns) => {
for (const { catchPattern, exactPattern, resolvedPath } of aliasPatterns) {
// Check if the import path starts with the alias pattern
const match = importPath.match(catchPattern);
if (match) {
const newMatch = match[0].replace(exactPattern, resolvedPath)
const newImportPath = importPath.replace(match[0], newMatch);
// Construct the new path by combining the resolved path with the remaining path
return importPath.replace(match, newImportPath)
}
}
return importPath; // No match found
},
replaceImports: (filePath, aliasPatterns) => {
let fileContent = fs.readFileSync(filePath, 'utf8');
// Replace import paths based on alias patterns
fileContent = fileContent.replace(/from ['"]([^'"]+)['"]/g, (match, importPath) => {
// Match the import path against alias patterns
const resolvedPath = this.#_helpers.aliases.matchImportPath(importPath, aliasPatterns);
// Only replace if the alias was matched, else return the original import path
if (resolvedPath !== importPath) {
// Preserve the specific path segments after the alias
const remainingPath = importPath.replace(/^.*\/[^\/]+/, ''); // Strip alias part
return `from '${resolvedPath}${remainingPath}${remainingPath.endsWith('.js') ? '' : '.js'}'`;
}
return match; // No change if alias was not matched
});
fs.writeFileSync(filePath, fileContent, 'utf8');
},
processFiles: (directory, aliasPatterns) => {
fs.readdirSync(directory).forEach(file => {
const fullPath = path.join(directory, file);
if (fs.lstatSync(fullPath).isDirectory()) {
this.#_helpers.aliases.processFiles(fullPath, aliasPatterns);
} else if (file.endsWith('.js')) {
this.#_helpers.aliases.replaceImports(fullPath, aliasPatterns);
}
});
},
check: () => {
if (!(utils.is.realObject(this.#_config.aliases) && Object.keys(this.#_config.aliases))) { return }
const aliasPatterns = this.#_helpers.aliases.getPatterns();
if (this.#_config.esmDir) {
this.#_helpers.aliases.processFiles(this.#_config.esmDir, aliasPatterns);
}
if (this.#_config.cjsDir) {
this.#_helpers.aliases.processFiles(this.#_config.cjsDir, aliasPatterns);
}
}
}
}
run() {
const st = Date.now();
this.#_helpers.config.check();
this.#_helpers.read();
this.#_helpers.create.packages();
this.#_helpers.copy.run();
this.#_helpers.extensions.run();
this.#_helpers.aliases.check();
const et = Date.now();
const duration = et - st;
this.#_helpers.print(`PostBuild finished in ${duration} milliseconds`);
}
}
export default new Main();