UNPKG

@putout/operator-filesystem

Version:

🐊Putout operator adds ability to filesystem referenced variables that was not defined

499 lines (359 loc) 12.8 kB
import { join, basename, dirname, } from 'node:path'; import {types} from '@putout/babel'; import {tryCatch} from 'try-catch'; import { setLiteralValue, getProperty, traverseProperties, } from '@putout/operate'; import * as maybeFS from './maybe-fs.js'; import {getRootFromAst} from './get-root-from-ast.js'; import { createTypeProperty, createFilesProperty, createFilenameProperty, createContentProperty, } from './property.js'; const { isProgram, objectExpression, } = types; const isString = (a) => typeof a === 'string'; const isSet = (a) => a instanceof Set; const {isArray} = Array; const maybeArray = (a) => isArray(a) ? a : [a]; const maybeArrayFrom = (a) => isSet(a) ? Array.from(a) : maybeArray(a); const escape = (a) => encodeURIComponent(a).replaceAll('%', '+'); const unescape = (a) => decodeURIComponent(a.replaceAll('+', '%')); const toBase64 = (content) => { const [e, result] = tryCatch(btoa, content); if (e) return btoa(escape(content)); return result; }; const fromBase64 = (content) => { if (content.includes(' ')) return content; const [e, decoded] = tryCatch(atob, content); if (!e) { if (!decoded.includes(' ') && decoded.includes('+')) return unescape(decoded); return decoded; } return content; }; const getRegExp = (wildcard) => { const escaped = wildcard .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace('?', '.?'); return RegExp(`^${escaped}$`); }; export function getParentDirectory(filePath) { if (!filePath.parentPath) return null; const {parentPath} = filePath.parentPath.parentPath; if (isProgram(parentPath)) return null; return parentPath; } function isExcluded({name, base, exclude}) { for (const currentExclude of exclude) { if (name === currentExclude || getRegExp(currentExclude).test(base)) return true; } return false; } export const crawlDirectory = (a) => traverseProperties(a, 'filename'); function parseFindFileOptions(options) { if (!options) return { excluded: [], }; if (isArray(options)) return { exclude: options, }; return options; } export function findFile(node, name, options) { const { exclude = [], crawled = crawlDirectory(node), } = parseFindFileOptions(options); checkName(name); const filePaths = new Set(); const names = maybeArrayFrom(name); for (const filenamePath of crawled) { const {value} = filenamePath.node.value; const base = basename(value); for (const name of names) { if (value === name || getRegExp(name).test(base)) { const path = filenamePath.parentPath; const excluded = isExcluded({ name, base, exclude, }); if (excluded) continue; filePaths.add(path); } } } return Array.from(filePaths); } function checkName(name) { if (!isString(name) && !isArray(name) && !isSet(name)) throw Error(`☝️ Looks like you forget to pass the 'name' of a file to 'findFile(filePath: Path|FilePath, name: string | string[] | Set<string>): FilePath'`); } function getFilenamePath(filePath) { const filenamePath = getProperty(filePath, 'filename'); return filenamePath.get('value'); } export function getFilename(filePath) { const {value} = getFilenamePath(filePath).node; return value; } export function getFileType(filePath) { const typePath = getProperty(filePath, 'type'); return typePath.node.value.value; } export function getFileContent(filePath) { const content = getProperty(filePath, 'content'); return [ Boolean(content), content?.node.value.value, ]; } function checkRenameFileName(name) { if (!isString(name)) throw Error(`☝️ Looks like you forget to pass the 'name' of a file to 'renameFile(filePath: FilePath, name: string)'`); } export const renameFile = (filePath, name) => { checkRenameFileName(name); const oldName = getFilename(filePath); const valuePath = getFilenamePath(filePath); const baseName = oldName .split('/') .pop(); const newName = name .split('/') .pop(); const newFilename = oldName.replace(baseName, newName); setLiteralValue(valuePath, newFilename); maybeFS.renameFile(oldName, newFilename); }; export function removeFile(filePath) { const filename = getFilename(filePath); if (!getParentDirectory(filePath)) return; filePath.remove(); maybeFS.removeFile(filename); } export const removeEmptyDirectory = (dirPath) => { const type = getFileType(dirPath); if (type !== 'directory') return; let nextParentDir = dirPath; while (!readDirectory(dirPath).length) { const name = getFilename(dirPath); if (name === '/') break; nextParentDir = getParentDirectory(dirPath); if (!nextParentDir) break; removeFile(dirPath); dirPath = nextParentDir; } }; export const moveFile = (filePath, dirPath) => { if (filePath === dirPath) return; const dirname = getFilename(dirPath); const filename = getFilename(filePath); const dirPathFiles = getFiles(dirPath); const filenamePath = getProperty(filePath, 'filename'); const basename = filename .split('/') .pop(); const newFilename = join(dirname, basename); maybeRemoveFile(dirPath, newFilename); setLiteralValue(filenamePath.get('value'), newFilename); dirPathFiles.node.value.elements.push(filePath.node); filePath.remove(); maybeFS.renameFile(filename, newFilename); }; export const copyFile = (filePath, dirPath) => { const dirname = getFilename(dirPath); const filename = getFilename(filePath); const basename = filename .split('/') .pop(); const newFilename = join(dirname, basename); const [hasContent, content] = getFileContent(filePath); const copiedFile = objectExpression([ createTypeProperty('file'), createFilenameProperty(newFilename), hasContent && createContentProperty(content), ].filter(Boolean)); maybeRemoveFile(dirPath, newFilename); const dirPathFiles = getFiles(dirPath); dirPathFiles.node.value.elements.push(copiedFile); maybeFS.copyFile(filename, newFilename); }; function maybeRemoveFile(dirPath, filename) { const type = getFileType(dirPath); if (type !== 'directory') { const filename = getFilename(dirPath); throw Error(`☝️ Looks like '${filename}' is not a directory, but: '${type}'. Rename to '${filename}/'`); } const dirPathFiles = getProperty(dirPath, 'files'); const name = join(getFilename(dirPath), basename(filename)); const [fileToOverwrite] = findFile(dirPathFiles, name); if (!fileToOverwrite) return; fileToOverwrite.remove(); } export const createFile = (dirPath, name, content) => { maybeRemoveFile(dirPath, name); const dirPathFiles = getFiles(dirPath); const parentFilename = getFilename(dirPath); const filename = join(parentFilename, name); const typeProperty = createTypeProperty('file'); const filenameProperty = createFilenameProperty(filename); const properties = [ typeProperty, filenameProperty, content && createContentProperty(content), ].filter(Boolean); dirPathFiles.node.value.elements.push(objectExpression(properties)); const filePath = dirPathFiles.get('value.elements').at(-1); if (isString(content)) writeFileContent(filePath, content); return filePath; }; const getFiles = (dirPath) => getProperty(dirPath, 'files'); export function readDirectory(dirPath) { const fileType = getFileType(dirPath); if (fileType !== 'directory') return []; return getFiles(dirPath).get('value.elements'); } export function createDirectory(dirPath, name) { const [existed] = getFile(dirPath, name); if (existed) { const fileType = getFileType(existed); if (fileType === 'directory') return existed; removeFile(existed); } const dirPathFiles = getFiles(dirPath); const parentFilename = getFilename(dirPath); const filename = join(parentFilename, name); const typeProperty = createTypeProperty('directory'); const filesProperty = createFilesProperty([]); const filenameProperty = createFilenameProperty(filename); dirPathFiles.node.value.elements.push(objectExpression([ typeProperty, filenameProperty, filesProperty, ])); maybeFS.createDirectory(filename); return dirPathFiles.get('value.elements').at(-1); } export const readFileContent = (filePath) => { const fileType = getFileType(filePath); if (fileType === 'directory') return ''; const [hasContent, content] = getFileContent(filePath); if (hasContent) return fromBase64(content); const filename = getFilename(filePath); const fileContent = maybeFS.readFileContent(filename); const property = createContentProperty(toBase64(fileContent)); filePath.node.properties.push(property); return fileContent; }; export function writeFileContent(filePath, content) { const fileType = getFileType(filePath); if (fileType === 'directory') return; const filename = getFilename(filePath); maybeFS.writeFileContent(filename, content); const contentPath = getProperty(filePath, 'content'); if (contentPath) { setLiteralValue(contentPath.node.value, toBase64(content)); return; } const property = createContentProperty(toBase64(content)); filePath.node.properties.push(property); } export const createNestedDirectory = (path, name) => { const rootPath = getRootDirectory(path); const dir = dirname(name); if (dir === getFilename(path)) return createDirectory(path, basename(name)); let currentDir = name; const rootDir = getFilename(rootPath); const directories = []; let prevDir = currentDir; while (currentDir !== rootDir) { directories.unshift(currentDir); prevDir = currentDir; currentDir = dirname(currentDir); if (currentDir === prevDir) { currentDir = rootDir; for (const [i, dir] of directories.entries()) { directories[i] = join(rootDir, dir); } directories.shift(); break; } } let lastDirectoryPath = findFile(rootPath, directories).at(-1) || rootPath; const lastDirectoryName = getFilename(lastDirectoryPath); const n = directories.length; let i = directories.indexOf(lastDirectoryName) + 1; for (; i < n; i++) { const name = basename(directories[i]); lastDirectoryPath = createDirectory(lastDirectoryPath, name); } return lastDirectoryPath; }; export function getRootDirectory(path) { if (path.program) return getRootFromAst(path); let currentDirPath = getParentDirectory(path); if (!currentDirPath) return path; let prevPath = currentDirPath; while (currentDirPath = getParentDirectory(currentDirPath)) { prevPath = currentDirPath; } return prevPath; } export function getFile(directoryPath, name, {type} = {}) { const names = maybeArray(name); const files = new Map(); let count = 0; for (const name of names) files.set(name, null); for (const currentFile of readDirectory(directoryPath)) { const currentName = getFilename(currentFile); const base = basename(currentName); if (!names.includes(base)) continue; if (type && type !== getFileType(currentFile)) continue; files.set(base, currentFile); ++count; if (names.length === count) break; } return files.values(); }