kt-extendscript-builder
Version:
Vite based builder for transpile TypeScript to ExtendScript
144 lines (143 loc) • 5.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cleaner = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const rimraf_1 = __importDefault(require("rimraf"));
const util_1 = require("util");
const rimrafAsync = (0, util_1.promisify)(rimraf_1.default);
/**
* Class responsible for safely cleaning output directories
*/
class Cleaner {
/**
* Safely cleans the output directory
* @param options Build options
*/
static async cleanDist(options) {
if (!this.validateOptions(options)) {
return;
}
if (!fs_1.default.existsSync(options.output)) {
console.warn(`Output file ${options.output} does not exist. Cleaning will not be performed.`);
return;
}
const stat = fs_1.default.lstatSync(options.output);
const isFile = stat.isFile();
const isDirectory = stat.isDirectory();
const distPath = isFile
? path_1.default.dirname(options.output)
: isDirectory
? path_1.default.resolve(options.output)
: '';
if (!this.validateDistPath(distPath)) {
return;
}
try {
await this.performClean(distPath, options.output);
}
catch (error) {
console.error(`Error cleaning ${distPath}:`, error);
}
}
/**
* Validates that the options are correct for cleaning
* @param options Build options
* @returns true if options are valid, false otherwise
*/
static validateOptions(options) {
if (!options.output) {
console.warn('No output file specified, cannot clean');
return false;
}
return true;
}
/**
* Verifies that the output directory exists and is safe to clean
* @param distPath Path to the output directory
* @returns true if the directory is valid and safe, false otherwise
*/
static validateDistPath(distPath) {
// Check if the directory exists
if (!fs_1.default.existsSync(distPath)) {
console.warn(`${distPath} does not exist. Skipping cleaning`);
return false;
}
const absolutePath = path_1.default.resolve(distPath);
const projectRoot = process.cwd();
// Check that the directory to clean is a subdirectory of the project
// and not the project root directory itself
if (!absolutePath.startsWith(projectRoot) || absolutePath === projectRoot) {
console.error(`Security warning! ${absolutePath} will not be cleaned as it is not a safe project subdirectory`);
return false;
}
// Verify it's not a protected directory
const relativePath = path_1.default.relative(projectRoot, absolutePath);
const pathParts = relativePath.split(path_1.default.sep);
for (const part of pathParts) {
// We check each segment of the path to verify it's not a protected folder
// We exclude paths like "dist.test" that contain "test" but are valid for cleaning
if (this.PROTECTED_FOLDERS.some((folder) => part === folder || // It's exactly a protected folder
(part === 'test' && pathParts[0] !== 'dist.test') // It's 'test' but not inside 'dist.test'
)) {
console.error(`Security warning! ${absolutePath} will not be cleaned as it is a protected project directory`);
return false;
}
}
return true;
}
/**
* Performs the cleaning of the directory or file
* @param distPath Path to the output directory
* @param outputPath Complete path to the output file
*/
static async performClean(distPath, outputPath) {
const outputFilename = path_1.default.basename(outputPath);
const outputFilePath = path_1.default.join(distPath, outputFilename);
// If the specific file exists, delete only that file
if (fs_1.default.existsSync(outputFilePath)) {
await fs_1.default.promises.unlink(outputFilePath);
console.log(`File cleaned: ${outputFilePath}`);
return;
}
// If it's a directory, delete its contents but keep the directory
const files = await fs_1.default.promises.readdir(distPath);
if (files.length === 0) {
console.log(`Directory ${distPath} is empty. Nothing to clean.`);
return;
}
// Safe deletion file by file
for (const file of files) {
const filePath = path_1.default.join(distPath, file);
await this.removeFileOrDirectory(filePath);
}
console.log(`Directory cleaned: ${distPath}`);
}
/**
* Safely removes a file or directory
* @param filePath Path to the file or directory to remove
*/
static async removeFileOrDirectory(filePath) {
const stats = await fs_1.default.promises.lstat(filePath);
if (stats.isDirectory()) {
// Additional security check for subdirectories
const dirName = path_1.default.basename(filePath);
if (this.PROTECTED_FOLDERS.includes(dirName)) {
console.warn(`Skipping protected folder: ${filePath}`);
return;
}
await rimrafAsync(filePath);
console.log(`Subdirectory removed: ${filePath}`);
}
else {
await fs_1.default.promises.unlink(filePath);
console.log(`File removed: ${filePath}`);
}
}
}
exports.Cleaner = Cleaner;
// Critical folders that should not be deleted
Cleaner.PROTECTED_FOLDERS = ['src', 'node_modules'];