diff --git a/.gitignore b/.gitignore index 4b231101e..5c70116cb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist *.log *.local playground/ +.playgroundcache \ No newline at end of file diff --git a/README.md b/README.md index 794345658..fb88bb1b2 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,16 @@ pnpm generate It will prompt you to select the desired language, then you can find the generated challenges in the `./playground` folder. +Later if you want to update playground while keeping your changes: + +```bash +pnpm generate --keep-changes +``` +OR +```bash +pnpm generate -K +``` + ## Thanks This project was born from solving real-world types problem with [@hardfist](https://github.com/hardfist) and [@MeCKodo](https://github.com/MeCKodo). And great thanks to [@sinoon](https://github.com/sinoon) who contributed a lot while giving early feedback on this project. diff --git a/scripts/generate-play.ts b/scripts/generate-play.ts index 498d87ec2..0959d14fe 100644 --- a/scripts/generate-play.ts +++ b/scripts/generate-play.ts @@ -1,21 +1,109 @@ import path from 'path' import { argv } from 'process' +import crypto from 'crypto' import fs from 'fs-extra' import c from 'picocolors' import prompts from 'prompts' import { formatToCode } from './actions/utils/formatToCode' import { loadQuizes, resolveInfo } from './loader' -import { defaultLocale, supportedLocales } from './locales' +import { supportedLocales } from './locales' import { getQuestionFullName } from './actions/issue-pr' import type { QuizMetaInfo } from './types' +type Snapshot = Record + +function calculateFileHash(filePathFull: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha1') + const fileStream = fs.createReadStream(filePathFull) + + fileStream.on('data', (data) => { + hash.update(data) + }) + + fileStream.on('end', () => { + hash.update(filePathFull) + resolve(hash.digest('hex')) + }) + + fileStream.on('error', (err) => { + reject(err) + }) + }) +} + +async function takeSnapshot(quizesPath: string) { + let snapshot: Snapshot = {} + + const files = fs.readdirSync(quizesPath) + + for (const file of files) { + // Might be a file, or a folder + const fPath = path.join(quizesPath, file) + const fStats = fs.statSync(fPath) + + if (fStats.isDirectory()) { + snapshot = { + ...snapshot, + ...(await takeSnapshot(fPath)), + } + } + else { + snapshot[file] = await calculateFileHash(fPath) + } + } + + return snapshot +} + +function readPlaygroundCache(playgroundCachePath: string): Snapshot { + if (!fs.existsSync(playgroundCachePath)) + return {} + + try { + const rawCacheContent = fs.readFileSync(playgroundCachePath) + return JSON.parse(rawCacheContent.toString()) + } + catch (err) { + console.log(c.red('Playground cache corrupted. ' + + 'Cannot generate playground without keeping your changes intact')) + console.log(c.cyan('Please ensure you have run this: "pnpm generate"')) + process.exit(1) + } +} + +function calculateOverridableFiles(cache: Snapshot, snapshot: Snapshot) { + const result: Snapshot = {} + + for (const quizName in snapshot) { + if (snapshot[quizName] === cache[quizName]) + result[quizName] = snapshot[quizName] + } + + return result +} + async function generatePlayground() { - const quizesPath = path.join(__dirname, '../playground') + const playgroundPath = path.join(__dirname, '../playground') + const playgroundCachePath = path.join(__dirname, '../.playgroundcache') + let locale = supportedLocales.find(locale => locale === argv[2])! - console.log(c.bold(c.cyan('Generateing local playground...\n'))) + console.log(c.bold(c.cyan('Generating local playground...\n'))) + + let overridableFiles: Snapshot = {} + let keepChanges = false + const currentPlaygroundCache = readPlaygroundCache(playgroundCachePath) - if (fs.existsSync(quizesPath)) { + if (argv.length === 3 && (argv[2] === '--keep-changes' || argv[2] === '-K')) { + console.log(c.bold(c.cyan('We will keep your chanegs while generating.\n'))) + keepChanges = true + + const playgroundSnapshot = await takeSnapshot(playgroundPath) + + overridableFiles = calculateOverridableFiles(currentPlaygroundCache, playgroundSnapshot) + } + else if (fs.existsSync(playgroundPath)) { const result = await prompts([{ name: 'confirm', type: 'confirm', @@ -41,20 +129,38 @@ async function generatePlayground() { locale = result.locale } - await fs.remove(quizesPath) - await fs.ensureDir(quizesPath) + if (!keepChanges) { + await fs.remove(playgroundPath) + await fs.ensureDir(playgroundPath) + } const quizes = await loadQuizes() + const incomingQuizesCache: Snapshot = {} + for (const quiz of quizes) { const { difficulty, title } = resolveInfo(quiz, locale) as QuizMetaInfo & { difficulty: string } const code = formatToCode(quiz, locale) - const filepath = path.join(quizesPath, `${getQuestionFullName(quiz.no, difficulty, title)}.ts`) - await fs.writeFile(filepath, code, 'utf-8') + const quizesPathByDifficulty = path.join(playgroundPath, difficulty) + + const quizFileName = `${getQuestionFullName(quiz.no, difficulty, title)}.ts` + const quizPathFull = path.join(quizesPathByDifficulty, quizFileName) + + if (!keepChanges || overridableFiles[quizFileName]) { + if (!fs.existsSync(quizesPathByDifficulty)) + fs.mkdirSync(quizesPathByDifficulty) + await fs.writeFile(quizPathFull, code, 'utf-8') + incomingQuizesCache[quizFileName] = await calculateFileHash(quizPathFull) + } } + fs.writeFile(playgroundCachePath, JSON.stringify({ + ...currentPlaygroundCache, + ...incomingQuizesCache, + })) + console.log() - console.log(c.bold(c.green('Local playground generated at: ')) + c.dim(quizesPath)) + console.log(c.bold(c.green('Local playground generated at: ')) + c.dim(playgroundPath)) console.log() }