sass-site/source/_data/releases.ts

124 lines
3.3 KiB
TypeScript
Raw Normal View History

2023-06-19 23:55:26 +02:00
import {spawn as nodeSpawn, SpawnOptionsWithoutStdio} from 'node:child_process';
2023-06-16 19:45:29 +02:00
import fs from 'node:fs/promises';
2023-02-02 16:44:29 +01:00
2023-06-16 19:45:29 +02:00
import deepEqual from 'deep-equal';
import kleur from 'kleur';
2023-06-19 23:55:26 +02:00
import {compare, parse} from 'semver';
2023-06-16 19:45:29 +02:00
type VersionCache = Record<string, string>;
2023-02-02 21:52:26 +01:00
2023-02-02 16:44:29 +01:00
const VERSION_CACHE_PATH = './source/_data/versionCache.json';
2023-03-08 21:59:19 +01:00
/**
* Promise version of `spawn` to avoid blocking the main thread while waiting
* for the child processes.
*/
2023-06-16 19:45:29 +02:00
const spawn = (
cmd: string,
args: string[],
2023-06-19 23:55:26 +02:00
options: SpawnOptionsWithoutStdio
2023-06-16 19:45:29 +02:00
) => {
2023-02-02 16:44:29 +01:00
return new Promise((resolve, reject) => {
const child = nodeSpawn(cmd, args, options);
2023-06-16 19:45:29 +02:00
const stderr: string[] = [];
const stdout: string[] = [];
child.stdout.on('data', (data: Buffer) => {
2023-02-02 16:44:29 +01:00
stdout.push(data.toString());
});
2023-06-16 19:45:29 +02:00
child.on('error', (e: Error) => {
2023-02-02 16:44:29 +01:00
stderr.push(e.toString());
});
child.on('close', () => {
2023-03-08 22:14:25 +01:00
if (stderr.length) {
reject(stderr.join(''));
} else {
resolve(stdout.join(''));
}
2023-02-02 16:44:29 +01:00
});
});
2023-03-08 22:14:25 +01:00
};
2023-02-02 16:44:29 +01:00
2023-03-08 21:59:19 +01:00
/**
* Retrieves cached version object from cache file.
*/
2023-03-08 22:14:25 +01:00
const getCacheFile = async () => {
2023-02-02 16:44:29 +01:00
let versionCache;
try {
2023-06-16 19:45:29 +02:00
const versionFile = await fs.readFile(VERSION_CACHE_PATH);
versionCache = JSON.parse(versionFile.toString()) as VersionCache;
2023-02-02 16:44:29 +01:00
} catch (err) {
2023-06-16 19:45:29 +02:00
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
2023-02-02 16:44:29 +01:00
versionCache = {}; // Cache is missing and needs to be created
} else {
throw err;
}
}
return versionCache;
2023-03-08 22:14:25 +01:00
};
2023-03-08 21:59:19 +01:00
/**
* Writes version object to cache file.
*/
2023-06-16 19:45:29 +02:00
const writeCacheFile = async (cache: VersionCache) => {
2023-06-19 23:55:26 +02:00
console.info(kleur.green('[11ty] Writing version cache file...'));
await fs.writeFile(VERSION_CACHE_PATH, JSON.stringify(cache));
2023-03-08 22:14:25 +01:00
};
2023-02-02 16:44:29 +01:00
2023-03-08 21:59:19 +01:00
/**
* Retrieves the highest stable version of `repo`, based on its git tags.
*/
2023-06-16 19:45:29 +02:00
const getLatestVersion = async (repo: string) => {
2023-06-15 19:49:54 +02:00
console.info(kleur.cyan(`[11ty] Fetching version information for ${repo}`));
2023-02-02 16:44:29 +01:00
let stdout;
try {
2023-06-16 19:45:29 +02:00
stdout = (await spawn(
2023-02-02 16:44:29 +01:00
'git',
['ls-remote', '--tags', '--refs', `https://github.com/${repo}`],
2023-06-19 23:55:26 +02:00
{env: {...process.env, GIT_TERMINAL_PROMPT: '0'}}
2023-06-16 19:45:29 +02:00
)) as string;
2023-02-02 16:44:29 +01:00
} catch (err) {
2023-06-15 19:49:54 +02:00
console.error(kleur.red(`[11ty] Failed to fetch git tags for ${repo}`));
2023-02-02 16:44:29 +01:00
throw err;
}
2023-06-16 19:45:29 +02:00
const isNotPreRelease = (version: string) => {
const parsed = parse(version);
return parsed && parsed.prerelease.length === 0;
2023-02-02 16:44:29 +01:00
};
const version = stdout
2023-02-02 16:44:29 +01:00
.split('\n')
2023-06-19 23:55:26 +02:00
.map(line => line.split('refs/tags/').at(-1) ?? '')
2023-02-02 16:44:29 +01:00
.filter(isNotPreRelease)
2023-06-16 19:45:29 +02:00
.sort(compare)
2023-02-02 21:52:26 +01:00
.at(-1);
2023-02-02 16:44:29 +01:00
2023-06-16 19:45:29 +02:00
return version ?? '';
2023-03-08 22:14:25 +01:00
};
2023-02-02 16:44:29 +01:00
2023-03-08 21:59:19 +01:00
/**
2023-06-16 19:45:29 +02:00
* Returns the version and URL for the latest release of all implementations.
2023-03-08 21:59:19 +01:00
*/
2023-02-02 16:44:29 +01:00
module.exports = async () => {
const repos = ['sass/libsass', 'sass/dart-sass', 'sass/migrator'];
const cache = await getCacheFile();
2023-02-03 23:58:02 +01:00
const versions = await Promise.all(
2023-06-19 23:55:26 +02:00
repos.map(async repo => [
2023-02-03 23:58:02 +01:00
repo,
2023-02-06 18:52:31 +01:00
cache[repo] ?? (await getLatestVersion(repo)),
2023-06-19 23:55:26 +02:00
])
2023-02-03 23:58:02 +01:00
);
const data = Object.fromEntries(
versions.map(([repo, version]) => [
repo.replace('sass/', ''),
2023-06-19 23:55:26 +02:00
{version, url: `https://github.com/${repo}/releases/tag/${version}`},
])
2023-02-03 23:58:02 +01:00
);
2023-06-16 19:45:29 +02:00
const nextCache = Object.fromEntries(versions) as VersionCache;
2023-03-08 22:14:25 +01:00
if (!deepEqual(cache, nextCache)) {
await writeCacheFile(nextCache);
}
2023-02-03 23:58:02 +01:00
2023-02-02 16:44:29 +01:00
return data;
};