UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

179 lines 6.33 kB
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; };