@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
179 lines • 6.33 kB
JavaScript
import { extract } from 'tar';
import { globby } from 'globby';
import { rimraf } from 'rimraf';
import checksum from 'checksum';
import path from 'path';
import fs from 'fs';
import { execSync, logger } from './../../utils/index.js';
const resolvePlatformPathModule = (platform = process.platform) => platform === 'win32' ? path.win32 : path.posix;
export const normalizePathForComparison = (targetPath, platform = process.platform) => {
if (typeof targetPath !== 'string' || targetPath.trim().length === 0) {
return null;
}
const pathModule = resolvePlatformPathModule(platform);
const normalizedPath = pathModule.normalize(pathModule.resolve(targetPath.trim()));
return platform === 'win32' ? normalizedPath.toLowerCase() : normalizedPath;
};
export const isPathWithinDirectory = (targetPath, directoryPath, platform = process.platform) => {
const pathModule = resolvePlatformPathModule(platform);
const normalizedTargetPath = normalizePathForComparison(targetPath, platform);
const normalizedDirectoryPath = normalizePathForComparison(directoryPath, platform);
if (!normalizedTargetPath || !normalizedDirectoryPath) {
return false;
}
return normalizedTargetPath === normalizedDirectoryPath || normalizedTargetPath.startsWith(`${normalizedDirectoryPath}${pathModule.sep}`);
};
const libraryHasChanged = async (name, libDir, targetDir, hashStore) => {
const hashFile = path.join(targetDir, 'node_modules', name, '.link-deps-hash');
const referenceContents = fs.existsSync(hashFile) ? fs.readFileSync(hashFile, 'utf8') : '';
const libFiles = await findFiles(libDir, targetDir);
const hashes = [];
for await (const file of libFiles) {
if (file) {
hashes.push(await getFileHash(path.join(libDir, file)));
}
}
const contents = libFiles.map((file, index) => `${hashes[index]} ${file}`).join('\n');
hashStore.file = hashFile;
hashStore.hash = contents;
if (referenceContents === '') {
logger.info('[link] First time linking');
return true;
}
if (contents === referenceContents) {
logger.info('[link] No changes');
return false;
}
const contentsLines = contents.split('\n');
const refLines = referenceContents.split('\n');
for (let i = 0; i < contentsLines.length; i++) {
if (contentsLines[i] !== refLines[i]) {
logger.info(`[link] Changed file: ${libFiles[i]}`);
break;
}
}
return true;
};
const findFiles = async (libDir, targetDir) => {
const ignore = ['**/*', '!node_modules', '!.git'];
if (isPathWithinDirectory(targetDir, libDir)) {
const relativeTargetDir = path.relative(libDir, targetDir);
const targetDirName = relativeTargetDir.split(path.sep)[0];
if (targetDirName && targetDirName !== '.') {
ignore.push(`!${targetDirName}`);
}
}
const files = await globby(ignore, {
gitignore: true,
cwd: libDir,
nodir: true
});
return files.sort();
};
const buildLibrary = (name, dir) => {
const libraryPkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
if (!libraryPkgJson.name === name) {
logger.error(`[link][ERROR] Mismatch in package name: found '${libraryPkgJson.name}', expected '${name}'`);
process.exit(1);
}
if (libraryPkgJson.scripts && libraryPkgJson.scripts.build) {
logger.info(`[link] Building ${name} in ${dir}`);
execSync('npm run build', {
cwd: dir,
stdio: 'inherit'
});
}
};
const packAndInstallLibrary = (name, dir, targetDir) => {
const libDestDir = path.join(targetDir, 'node_modules', name);
let fullPackageName;
try {
logger.info('[link] Copying to local node_modules');
execSync('npm pack', {
cwd: dir
});
if (fs.existsSync(libDestDir)) {
rimraf.sync(libDestDir);
}
fs.mkdirSync(libDestDir, {
recursive: true
});
const tmpName = name.replace(/[\s\/]/g, '-').replace(/@/g, '');
const regex = new RegExp(`^(at-)?${tmpName}(.*).tgz$`);
const packagedName = fs.readdirSync(dir).find(file => regex.test(file));
fullPackageName = path.join(dir, packagedName);
logger.info(`[link] Extracting "${fullPackageName}" to ${libDestDir}`);
const [cwd, file] = [libDestDir, fullPackageName].map(absolutePath => path.relative(process.cwd(), absolutePath));
extract({
cwd,
file,
gzip: true,
stripComponents: 1,
sync: true
});
} finally {
if (fullPackageName) {
fs.unlinkSync(fullPackageName);
}
}
};
const getFileHash = async file => await new Promise((resolve, reject) => {
checksum.file(file, (error, hash) => {
if (error) {
reject(error);
} else {
resolve(hash);
}
});
});
const clearCache = () => {
const target = './app/node_modules/.vite';
if (fs.existsSync(target)) {
logger.info('[link] Clearing Vite dependency cache');
rimraf.sync(target);
}
};
const getLinkJson = appDir => JSON.parse(fs.readFileSync(path.join(appDir, 'link.json'), 'utf-8'));
export default async filePath => {
const deps = new Map();
const appDir = path.join(process.cwd(), 'app');
const linkJsonPath = path.join(appDir, 'link.json');
if (fs.existsSync(linkJsonPath)) {
const linkJson = getLinkJson(appDir);
for (const [key, value] of Object.entries(linkJson)) {
const src = value.src || value.path;
const libDir = path.resolve(appDir, src);
if (value.enabled) {
if (filePath && !isPathWithinDirectory(filePath, libDir)) {
continue;
}
logger.info(`[link] Linking ${key}...`);
deps.set(key, libDir);
}
}
}
if (deps.size > 0) {
const targetDir = appDir;
const depNames = deps.keys();
for await (const name of depNames) {
const libDir = deps.get(name);
const hashStore = {
hash: '',
file: ''
};
if (!fs.existsSync(libDir)) {
logger.error(`[link][ERROR] Directory ${libDir} does not exist`);
process.exit(1);
}
const hasChanges = await libraryHasChanged(name, libDir, targetDir, hashStore);
if (hasChanges) {
clearCache();
buildLibrary(name, libDir);
packAndInstallLibrary(name, libDir, targetDir);
fs.writeFileSync(hashStore.file, hashStore.hash);
logger.info(`[link] Re-installing ${name}...`);
}
}
}
return deps;
};