import * as fs from 'fs-extra'; import { execCommand, rootDir } from './tool-utils'; const md5File = require('md5-file'); import * as glob from 'glob'; const clipperDir = `${rootDir}/packages/app-clipper`; const tmpSourceDirName = 'Clipper-source'; async function copyDir(baseSourceDir: string, sourcePath: string, baseDestDir: string) { await fs.mkdirp(`${baseDestDir}/${sourcePath}`); await fs.copy(`${baseSourceDir}/${sourcePath}`, `${baseDestDir}/${sourcePath}`); } async function copyToDist(distDir: string) { await copyDir(clipperDir, 'popup/build', distDir); await copyDir(clipperDir, 'content_scripts', distDir); await copyDir(clipperDir, 'icons', distDir); await copyDir(clipperDir, 'util', distDir); await fs.copy(`${clipperDir}/service_worker.mjs`, `${distDir}/service_worker.mjs`); await fs.copy(`${clipperDir}/manifest.json`, `${distDir}/manifest.json`); await fs.remove(`${distDir}/popup/build/manifest.json`); } async function updateManifestVersionNumber(manifestPath: string) { const manifestText = await fs.readFile(manifestPath, 'utf-8'); const manifest = JSON.parse(manifestText); const v = manifest.version.split('.'); const buildNumber = Number(v.pop()) + 1; v.push(buildNumber); manifest.version = v.join('.'); console.info(`New version: ${manifest.version}`); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 4)); return manifest.version; } async function createSourceZip() { const tmpSourceDir = `${__dirname}/../${tmpSourceDirName}`; const filename = 'joplin-webclipper-source.zip'; const filePath = `${clipperDir}/dist/${filename}`; console.info('Creating source tarball for code validation...'); console.info(`Chdir: ${clipperDir}/..`); process.chdir(`${clipperDir}/..`); console.info(await execCommand(`rm -f "${filePath}"`)); console.info(await execCommand(`rsync -a --delete --exclude 'node_modules/' --exclude 'build/' --exclude 'dist/' "${clipperDir}/" "${tmpSourceDir}/"`)); console.info(await execCommand(`7z a -tzip ${filename} "${tmpSourceDirName}"`)); console.info(await execCommand(`mv ${filename} "${clipperDir}/dist/" && rm -rf "${tmpSourceDirName}"`)); return filePath; } async function compareFiles(path1: string, path2: string) { return await md5File(path1) === await md5File(path2); } async function compareDir(dir1: string, dir2: string) { console.info(`Comparing directories ${dir1} to ${dir2}`); const globOptions = { ignore: [ '**/node_modules/**', '**/.git/**', ], }; const filterFiles = (f: string) => { const stat = fs.statSync(f); return !stat.isDirectory(); }; const files1 = glob.sync(`${dir1}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir1.length + 1)); const files2 = glob.sync(`${dir2}/**/*`, globOptions).filter(filterFiles).map(f => f.substr(dir2.length + 1)); const missingFiles1: string[] = []; const missingFiles2: string[] = []; const canBeMissing1: string[] = []; const canBeMissing2: string[] = ['manifest.json']; const differentFiles: string[] = []; for (const f of files1) { if (!files2.includes(f)) { if (canBeMissing2.includes(f)) continue; missingFiles2.push(f); continue; } const sameFiles = await compareFiles(`${dir1}/${f}`, `${dir2}/${f}`); if (!sameFiles) differentFiles.push(f); } for (const f of files2) { if (!files1.includes(f)) { if (canBeMissing1.includes(f)) continue; missingFiles1.push(f); } } if (missingFiles1.length) console.info(`Missing from ${dir1}:`, missingFiles1); if (missingFiles2.length) console.info(`Missing from ${dir2}:`, missingFiles2); if (differentFiles.length) console.info(`Different files: ${differentFiles}`); if (!differentFiles.length && !missingFiles1.length && !missingFiles2.length) { console.info('All files are equal'); return true; } return false; } async function checkSourceZip(sourceZip: string, compiledZip: string) { const tmpDir = `${require('os').tmpdir()}/${Date.now()}`; console.info(`Checking source ZIP in ${tmpDir}`); const sourceDir = `${tmpDir}/source`; const compiledDir = `${tmpDir}/compiled`; await fs.mkdirp(sourceDir); await fs.mkdirp(compiledDir); process.chdir(sourceDir); console.info(await execCommand(`cp "${sourceZip}" .`)); console.info(await execCommand(`unzip "${sourceZip}"`)); process.chdir(`${sourceDir}/Clipper-source/popup`); console.info(await execCommand('npm install')); console.info(await execCommand('npm run build')); process.chdir(compiledDir); console.info(await execCommand(`cp "${compiledZip}" .`)); console.info(await execCommand(`unzip "${compiledZip}"`)); const areEqual = await compareDir(`${sourceDir}/Clipper-source/popup/build`, `${compiledDir}/popup/build`); if (areEqual) { await fs.remove(sourceDir); await fs.remove(compiledDir); } } async function setReleaseMode(isReleaseMode: boolean) { const joplinEnvPath = `${clipperDir}/util/joplinEnv.mjs`; await fs.writeFile(joplinEnvPath, `// AUTOGENERATED by release-clipper\n\nexport default () => '${isReleaseMode ? 'prod' : 'dev'}';`); } async function main() { console.info(await execCommand('git pull')); const newVersion = await updateManifestVersionNumber(`${clipperDir}/manifest.json`); console.info('Building extension...'); process.chdir(`${clipperDir}/popup`); await setReleaseMode(true); console.info(await execCommand(`rm -rf ${clipperDir}/popup/build`)); console.info(await execCommand('npm run build')); type PlatformDistOptions = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied removeManifestKeys(manifest: Record): Record; outputPath?: string; }; const dists: Record = { chrome: { removeManifestKeys: (manifest) => { manifest = { ...manifest }; delete manifest.browser_specific_settings; manifest.background = { ...manifest.background }; delete manifest.background.scripts; delete manifest.background.persistent; return manifest; }, }, firefox: { removeManifestKeys: (manifest) => { manifest = { ...manifest }; manifest.background = { ...manifest.background }; delete manifest.background.service_worker; return manifest; }, }, }; for (const distName in dists) { const dist = dists[distName]; const distDir = `${clipperDir}/dist/${distName}`; await fs.remove(distDir); await fs.mkdirp(distDir); await copyToDist(distDir); const manifestText = await fs.readFile(`${distDir}/manifest.json`, 'utf-8'); let manifest = JSON.parse(manifestText); manifest.name = 'Joplin Web Clipper'; if (dist.removeManifestKeys) manifest = dist.removeManifestKeys(manifest); await fs.writeFile(`${distDir}/manifest.json`, JSON.stringify(manifest, null, 4)); process.chdir(distDir); console.info(await execCommand(`rm -f "${distName}.zip"`)); console.info(await execCommand(`7z a -tzip ${distName}.zip *`)); console.info(await execCommand(`mv ${distName}.zip ..`)); dists[distName].outputPath = `${clipperDir}/dist/${distName}.zip`; } const sourceZip = await createSourceZip(); await checkSourceZip(sourceZip, dists.firefox.outputPath); await setReleaseMode(false); process.chdir(clipperDir); if (!process.argv.includes('--no-publish')) { console.info(await execCommand('git add -A')); console.info(await execCommand(`git commit -m "Clipper release v${newVersion}"`)); console.info(await execCommand(`git tag clipper-${newVersion}`)); console.info(await execCommand('git push')); console.info(await execCommand('git push --tags')); } } main().catch((error) => { console.error('Fatal error'); console.error(error); process.exit(1); });