mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix(docs): correct versioned nav links and local dev version routing (#74)
This commit is contained in:
@@ -1,10 +1,27 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
readlinkSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import {
|
||||
collectSharedAssetPaths,
|
||||
dedupeVersionedPublicAssets,
|
||||
pruneArchiveCacheGenerations,
|
||||
} from './docs-versioned-assets';
|
||||
import {
|
||||
buildVersionManifest,
|
||||
stableTagsWithDocs,
|
||||
versionArchiveCacheKey,
|
||||
versionArchiveCacheName,
|
||||
versionOutputPath,
|
||||
versionPath,
|
||||
@@ -18,7 +35,11 @@ const archiveCacheRoot = join(repoRoot, '.tmp/docs-versioned-archive-cache');
|
||||
const maxCloudflareFiles = 20_000;
|
||||
const maxCloudflareFileBytes = 25 * 1024 * 1024;
|
||||
|
||||
function run(command: string, args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}) {
|
||||
function run(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
||||
) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
env: options.env ?? process.env,
|
||||
@@ -144,12 +165,14 @@ function buildDocs(options: {
|
||||
latestStable: string;
|
||||
manifestJson: string;
|
||||
}) {
|
||||
console.info(`[docs] building ${options.version ?? options.channel} -> ${options.base}`);
|
||||
run('bun', ['run', '--cwd', currentDocsSite, 'vitepress', 'build', options.snapshotDocsSite], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
SUBMINER_DOCS_BASE: options.base,
|
||||
SUBMINER_DOCS_OUT_DIR: options.outDir,
|
||||
SUBMINER_DOCS_SOURCE_DIR: options.snapshotDocsSite,
|
||||
SUBMINER_DOCS_CHANNEL: options.channel,
|
||||
SUBMINER_DOCS_VERSION: options.version ?? '',
|
||||
SUBMINER_DOCS_LATEST_STABLE: options.latestStable,
|
||||
@@ -160,25 +183,28 @@ function buildDocs(options: {
|
||||
}
|
||||
|
||||
function updateHashWithPath(hash: ReturnType<typeof createHash>, path: string) {
|
||||
if (isGeneratedVitePressPath(path)) {
|
||||
if (isSharedInternalsHashIgnoredPath(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = lstatSync(path);
|
||||
hash.update(path.replace(repoRoot, ''));
|
||||
hash.update(String(stat.mode));
|
||||
const relativePath = path.replace(repoRoot, '');
|
||||
|
||||
if (stat.isSymbolicLink()) {
|
||||
hash.update(`symlink:${relativePath}`);
|
||||
hash.update(readlinkSync(path));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
hash.update(`dir:${relativePath}`);
|
||||
for (const entry of readdirSync(path).sort()) {
|
||||
updateHashWithPath(hash, join(path, entry));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
hash.update(`file:${relativePath}`);
|
||||
hash.update(readFileSync(path));
|
||||
}
|
||||
|
||||
@@ -186,8 +212,15 @@ function isGeneratedVitePressPath(path: string): boolean {
|
||||
return /[\\/]\\.vitepress[\\/](cache|dist)([\\/]|$)/.test(path);
|
||||
}
|
||||
|
||||
function isSharedInternalsHashIgnoredPath(path: string): boolean {
|
||||
return isGeneratedVitePressPath(path) || /\.test\.[cm]?[jt]s$/.test(path);
|
||||
}
|
||||
|
||||
function computeSharedInternalsHash(): string {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(
|
||||
`version-link-origin:${process.env.SUBMINER_DOCS_VERSION_LINK_ORIGIN === 'local' ? 'local' : 'production'}`,
|
||||
);
|
||||
const paths = [
|
||||
join(currentDocsSite, '.vitepress'),
|
||||
join(currentDocsSite, 'public/assets/fonts'),
|
||||
@@ -216,6 +249,7 @@ function restoreCachedArchive(version: string, sharedInternalsHash: string): boo
|
||||
return false;
|
||||
}
|
||||
|
||||
console.info(`[docs] cache hit ${version}`);
|
||||
cpSync(cachedArchive, join(aggregateOutDir, versionOutputPath(version)), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
@@ -267,9 +301,7 @@ function assertCloudflarePagesLimits(root: string) {
|
||||
}
|
||||
|
||||
if (oversizedFiles.length > 0) {
|
||||
throw new Error(
|
||||
`Versioned docs output has files over 25 MiB:\n${oversizedFiles.join('\n')}`,
|
||||
);
|
||||
throw new Error(`Versioned docs output has files over 25 MiB:\n${oversizedFiles.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +316,9 @@ function main() {
|
||||
const manifest = buildVersionManifest({ latestStable, stableVersions });
|
||||
const manifestJson = JSON.stringify(manifest);
|
||||
const sharedInternalsHash = computeSharedInternalsHash();
|
||||
const archiveCacheKey = versionArchiveCacheKey({ sharedInternalsHash, manifestJson });
|
||||
const sharedAssetPaths = collectSharedAssetPaths(join(currentDocsSite, 'public/assets'));
|
||||
console.info(`[docs] archive cache key ${archiveCacheKey.slice(0, 12)}`);
|
||||
|
||||
rmSync(buildRoot, { recursive: true, force: true });
|
||||
rmSync(aggregateOutDir, { recursive: true, force: true });
|
||||
@@ -302,11 +337,13 @@ function main() {
|
||||
});
|
||||
|
||||
for (const version of stableVersions) {
|
||||
if (restoreCachedArchive(version, sharedInternalsHash)) {
|
||||
if (restoreCachedArchive(version, archiveCacheKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const snapshot = version === latestStable ? latestStableSnapshot : prepareSnapshot(version, version);
|
||||
console.info(`[docs] rebuilding archive ${version}`);
|
||||
const snapshot =
|
||||
version === latestStable ? latestStableSnapshot : prepareSnapshot(version, version);
|
||||
buildDocs({
|
||||
snapshotDocsSite: snapshot,
|
||||
base: versionPath(version),
|
||||
@@ -316,7 +353,12 @@ function main() {
|
||||
latestStable,
|
||||
manifestJson,
|
||||
});
|
||||
saveArchiveCache(version, sharedInternalsHash);
|
||||
dedupeVersionedPublicAssets({
|
||||
outDir: join(aggregateOutDir, versionOutputPath(version)),
|
||||
base: versionPath(version),
|
||||
sharedAssetPaths,
|
||||
});
|
||||
saveArchiveCache(version, archiveCacheKey);
|
||||
}
|
||||
|
||||
const mainSnapshot = prepareSnapshot('main');
|
||||
@@ -329,9 +371,22 @@ function main() {
|
||||
latestStable,
|
||||
manifestJson,
|
||||
});
|
||||
dedupeVersionedPublicAssets({
|
||||
outDir: join(aggregateOutDir, 'main'),
|
||||
base: '/main/',
|
||||
sharedAssetPaths,
|
||||
});
|
||||
|
||||
writeFileSync(join(aggregateOutDir, 'versions.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
assertCloudflarePagesLimits(aggregateOutDir);
|
||||
const prunedArchives = pruneArchiveCacheGenerations({
|
||||
cacheRoot: archiveCacheRoot,
|
||||
activeCacheKey: archiveCacheKey,
|
||||
});
|
||||
if (prunedArchives.length > 0) {
|
||||
console.info(`[docs] pruned ${prunedArchives.length} stale archive cache directories`);
|
||||
}
|
||||
rmSync(buildRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
dedupeVersionedPublicAssets,
|
||||
pruneArchiveCacheGenerations,
|
||||
rewriteSharedAssetReferences,
|
||||
} from './docs-versioned-assets';
|
||||
|
||||
function tempDir() {
|
||||
return mkdtempSync(join(tmpdir(), 'subminer-docs-versioned-assets-'));
|
||||
}
|
||||
|
||||
describe('docs versioned asset dedupe', () => {
|
||||
test('rewrites version-scoped public asset references to shared root assets', () => {
|
||||
const html =
|
||||
'<link href="/v/0.14.0/assets/style.hash.css"><source src="/v/0.14.0/assets/minecard.webm">';
|
||||
const expected =
|
||||
'<link href="/v/0.14.0/assets/style.hash.css"><source src="/assets/minecard.webm">';
|
||||
|
||||
expect(rewriteSharedAssetReferences(html, '/v/0.14.0/', new Set(['minecard.webm']))).toBe(
|
||||
expected,
|
||||
);
|
||||
expect(rewriteSharedAssetReferences(html, '/v/0.14.0', new Set(['minecard.webm']))).toBe(
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not rewrite longer asset paths with a shared asset prefix', () => {
|
||||
expect(
|
||||
rewriteSharedAssetReferences(
|
||||
[
|
||||
'<script src="/v/0.14.0/assets/foo.js"></script>',
|
||||
'<script src="/v/0.14.0/assets/foo.js?v=1"></script>',
|
||||
'<script src="/v/0.14.0/assets/foo.js.map"></script>',
|
||||
].join(''),
|
||||
'/v/0.14.0/',
|
||||
new Set(['foo.js']),
|
||||
),
|
||||
).toBe(
|
||||
[
|
||||
'<script src="/assets/foo.js"></script>',
|
||||
'<script src="/assets/foo.js?v=1"></script>',
|
||||
'<script src="/v/0.14.0/assets/foo.js.map"></script>',
|
||||
].join(''),
|
||||
);
|
||||
});
|
||||
|
||||
test('removes duplicated version public assets while preserving generated VitePress assets', async () => {
|
||||
const dir = tempDir();
|
||||
try {
|
||||
mkdirSync(join(dir, 'assets/chunks'), { recursive: true });
|
||||
writeFileSync(join(dir, 'assets/style.hash.css'), 'body{}');
|
||||
writeFileSync(join(dir, 'assets/chunks/theme.hash.js'), 'export {};');
|
||||
writeFileSync(join(dir, 'assets/minecard.webm'), 'large video');
|
||||
writeFileSync(
|
||||
join(dir, 'index.html'),
|
||||
'<link href="/v/0.14.0/assets/style.hash.css"><source src="/v/0.14.0/assets/minecard.webm">',
|
||||
);
|
||||
|
||||
const result = dedupeVersionedPublicAssets({
|
||||
outDir: dir,
|
||||
base: '/v/0.14.0',
|
||||
sharedAssetPaths: new Set(['minecard.webm']),
|
||||
});
|
||||
|
||||
expect(result.rewrittenFiles).toEqual([join(dir, 'index.html')]);
|
||||
expect(existsSync(join(dir, 'assets/style.hash.css'))).toBe(true);
|
||||
expect(existsSync(join(dir, 'assets/chunks/theme.hash.js'))).toBe(true);
|
||||
expect(existsSync(join(dir, 'assets/minecard.webm'))).toBe(false);
|
||||
expect(readFileSync(join(dir, 'index.html'), 'utf8')).toBe(
|
||||
'<link href="/v/0.14.0/assets/style.hash.css"><source src="/assets/minecard.webm">',
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps root public assets because they are the shared copy', async () => {
|
||||
const dir = tempDir();
|
||||
try {
|
||||
mkdirSync(join(dir, 'assets'), { recursive: true });
|
||||
writeFileSync(join(dir, 'assets/minecard.webm'), 'large video');
|
||||
|
||||
const result = dedupeVersionedPublicAssets({
|
||||
outDir: dir,
|
||||
base: '/',
|
||||
sharedAssetPaths: new Set(['minecard.webm']),
|
||||
});
|
||||
|
||||
expect(result.removedAssetsDir).toBe(false);
|
||||
expect(existsSync(join(dir, 'assets'))).toBe(true);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('docs archive cache pruning', () => {
|
||||
test('removes stale cache generations while keeping the active generation', async () => {
|
||||
const dir = tempDir();
|
||||
try {
|
||||
mkdirSync(join(dir, 'active123456-v0.14.0'), { recursive: true });
|
||||
mkdirSync(join(dir, 'stale654321-v0.14.0'), { recursive: true });
|
||||
mkdirSync(join(dir, 'stale654321-v0.13.0'), { recursive: true });
|
||||
|
||||
const removed = pruneArchiveCacheGenerations({
|
||||
cacheRoot: dir,
|
||||
activeCacheKey: 'active123456abcdef',
|
||||
});
|
||||
|
||||
expect(removed.sort()).toEqual([
|
||||
join(dir, 'stale654321-v0.13.0'),
|
||||
join(dir, 'stale654321-v0.14.0'),
|
||||
]);
|
||||
expect(existsSync(join(dir, 'active123456-v0.14.0'))).toBe(true);
|
||||
expect(existsSync(join(dir, 'stale654321-v0.14.0'))).toBe(false);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
|
||||
const textOutputPattern = /\.(?:css|html|js|json|map|mjs|txt|xml)$/;
|
||||
|
||||
function normalizeBase(base: string): string {
|
||||
return base === '/' ? '/' : `${base.replace(/\/+$/, '')}/`;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function collectSharedAssetPaths(publicAssetsDir: string): Set<string> {
|
||||
if (!existsSync(publicAssetsDir)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(
|
||||
walkFiles(publicAssetsDir).map((file) => relative(publicAssetsDir, file).split('\\').join('/')),
|
||||
);
|
||||
}
|
||||
|
||||
export function rewriteSharedAssetReferences(
|
||||
content: string,
|
||||
base: string,
|
||||
sharedAssetPaths: Set<string>,
|
||||
): string {
|
||||
const normalizedBase = normalizeBase(base);
|
||||
if (normalizedBase === '/') {
|
||||
return content;
|
||||
}
|
||||
|
||||
let rewritten = content;
|
||||
const escapedBase = escapeRegExp(normalizedBase);
|
||||
for (const assetPath of sharedAssetPaths) {
|
||||
const escapedAssetPath = escapeRegExp(assetPath);
|
||||
rewritten = rewritten.replace(
|
||||
new RegExp(`${escapedBase}assets/${escapedAssetPath}(?=$|[?#"'()\\s<])`, 'g'),
|
||||
`/assets/${assetPath}`,
|
||||
);
|
||||
}
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
function walkFiles(root: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function walk(dir: string) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const path = join(dir, entry);
|
||||
const stat = lstatSync(path);
|
||||
if (stat.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
walk(path);
|
||||
continue;
|
||||
}
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
walk(root);
|
||||
return files;
|
||||
}
|
||||
|
||||
export function dedupeVersionedPublicAssets(options: {
|
||||
outDir: string;
|
||||
base: string;
|
||||
sharedAssetPaths: Set<string>;
|
||||
}): {
|
||||
removedAssetsDir: boolean;
|
||||
rewrittenFiles: string[];
|
||||
} {
|
||||
const normalizedBase = normalizeBase(options.base);
|
||||
const rewrittenFiles: string[] = [];
|
||||
|
||||
for (const file of walkFiles(options.outDir)) {
|
||||
if (!textOutputPattern.test(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const before = readFileSync(file, 'utf8');
|
||||
const after = rewriteSharedAssetReferences(before, normalizedBase, options.sharedAssetPaths);
|
||||
if (after === before) {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeFileSync(file, after);
|
||||
rewrittenFiles.push(file);
|
||||
}
|
||||
|
||||
const assetsDir = join(options.outDir, 'assets');
|
||||
if (normalizedBase !== '/') {
|
||||
for (const assetPath of options.sharedAssetPaths) {
|
||||
rmSync(join(assetsDir, assetPath), { force: true });
|
||||
}
|
||||
removeEmptyDirectories(assetsDir);
|
||||
}
|
||||
|
||||
const removedAssetsDir = !existsSync(assetsDir);
|
||||
return { removedAssetsDir, rewrittenFiles };
|
||||
}
|
||||
|
||||
function removeEmptyDirectories(root: string) {
|
||||
if (!existsSync(root) || !lstatSync(root).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(root)) {
|
||||
const path = join(root, entry);
|
||||
if (lstatSync(path).isDirectory()) {
|
||||
removeEmptyDirectories(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (readdirSync(root).length === 0) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneArchiveCacheGenerations(options: {
|
||||
cacheRoot: string;
|
||||
activeCacheKey: string;
|
||||
}): string[] {
|
||||
if (!existsSync(options.cacheRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activePrefix = options.activeCacheKey.slice(0, 12);
|
||||
const removed: string[] = [];
|
||||
|
||||
for (const entry of readdirSync(options.cacheRoot)) {
|
||||
const path = join(options.cacheRoot, entry);
|
||||
if (!lstatSync(path).isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.startsWith(`${activePrefix}-`)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rmSync(path, { recursive: true, force: true });
|
||||
removed.push(path);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
buildVersionManifest,
|
||||
compareStableVersionsDesc,
|
||||
versionArchiveCacheKey,
|
||||
isStableReleaseTag,
|
||||
stableTagsWithDocs,
|
||||
versionArchiveCacheName,
|
||||
@@ -50,6 +51,19 @@ describe('docs versioning helpers', () => {
|
||||
expect(versionArchiveCacheName('v0.14.0', 'abcdef1234567890')).toBe('abcdef123456-v0.14.0');
|
||||
});
|
||||
|
||||
test('archive cache keys change when manifest contents change', () => {
|
||||
const firstKey = versionArchiveCacheKey({
|
||||
sharedInternalsHash: 'abcdef1234567890',
|
||||
manifestJson: '{"latestStable":"v0.14.0"}',
|
||||
});
|
||||
const secondKey = versionArchiveCacheKey({
|
||||
sharedInternalsHash: 'abcdef1234567890',
|
||||
manifestJson: '{"latestStable":"v0.15.0"}',
|
||||
});
|
||||
|
||||
expect(firstKey).not.toBe(secondKey);
|
||||
});
|
||||
|
||||
test('archive output paths stay relative for filesystem joins', () => {
|
||||
expect(versionOutputPath('v0.14.0')).toBe('v/0.14.0');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export type DocsVersionEntry = {
|
||||
version: string;
|
||||
path: string;
|
||||
@@ -57,14 +59,23 @@ export function versionArchiveCacheName(version: string, sharedInternalsHash: st
|
||||
return `${sharedInternalsHash.slice(0, 12)}-${version}`;
|
||||
}
|
||||
|
||||
export function versionArchiveCacheKey(options: {
|
||||
sharedInternalsHash: string;
|
||||
manifestJson: string;
|
||||
}): string {
|
||||
const hash = createHash('sha256');
|
||||
hash.update('shared-internals:');
|
||||
hash.update(options.sharedInternalsHash);
|
||||
hash.update('\nmanifest:');
|
||||
hash.update(options.manifestJson);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
export function stableTagsWithDocs(
|
||||
tags: string[],
|
||||
hasDocsSite: (tag: string) => boolean,
|
||||
): string[] {
|
||||
return tags
|
||||
.filter(isStableReleaseTag)
|
||||
.filter(hasDocsSite)
|
||||
.sort(compareStableVersionsDesc);
|
||||
return tags.filter(isStableReleaseTag).filter(hasDocsSite).sort(compareStableVersionsDesc);
|
||||
}
|
||||
|
||||
export function buildVersionManifest(options: {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import { buildVersionManifest, stableTagsWithDocs } from './docs-versioning';
|
||||
|
||||
const repoRoot = resolve(__dirname, '..');
|
||||
|
||||
function capture(command: string, args: string[]): string {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || `Command failed: ${command} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
function tagHasDocsSite(tag: string): boolean {
|
||||
const result = spawnSync('git', ['cat-file', '-e', `${tag}:docs-site/package.json`], {
|
||||
cwd: repoRoot,
|
||||
});
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
const stableVersions = stableTagsWithDocs(
|
||||
capture('git', ['tag', '--list', 'v*'])
|
||||
.split('\n')
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
tagHasDocsSite,
|
||||
);
|
||||
|
||||
const latestStable = stableVersions[0];
|
||||
|
||||
if (!latestStable) {
|
||||
throw new Error('No stable release tags with docs-site/package.json found.');
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(buildVersionManifest({ latestStable, stableVersions })));
|
||||
Reference in New Issue
Block a user