chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @param {{ name: string, version: string, private?: boolean }} pkg
|
||||
* @param {string | null} registryVersion
|
||||
* @returns {{ publish: boolean, reason: string }}
|
||||
*/
|
||||
export function shouldPublish(pkg, registryVersion) {
|
||||
if (pkg.private) {
|
||||
return { publish: false, reason: "private" };
|
||||
}
|
||||
|
||||
if (registryVersion === pkg.version) {
|
||||
return { publish: false, reason: "already published" };
|
||||
}
|
||||
|
||||
if (registryVersion === null) {
|
||||
return { publish: true, reason: "new package" };
|
||||
}
|
||||
|
||||
return { publish: true, reason: "version changed" };
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @param {string} version
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidVersion(version) {
|
||||
return /^\d+\.\d+\.\d+(-[\w.]+)?$/.test(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} packageJsonRaws - Array of raw JSON strings
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function collectWorkspaceNames(packageJsonRaws) {
|
||||
const names = new Set();
|
||||
for (const raw of packageJsonRaws) {
|
||||
const pkg = JSON.parse(raw);
|
||||
if (pkg.name) names.add(pkg.name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} raw - Raw JSON string of a package.json
|
||||
* @param {string} newVersion
|
||||
* @param {Set<string>} workspaceNames
|
||||
* @returns {string | null} Updated JSON string, or null if no change needed
|
||||
*/
|
||||
export function updatePackageJson(raw, newVersion, workspaceNames) {
|
||||
const pkg = JSON.parse(raw);
|
||||
|
||||
// Set new version (skip root which has no version field)
|
||||
if (pkg.version !== undefined) {
|
||||
pkg.version = newVersion;
|
||||
}
|
||||
|
||||
// Update internal dependency references
|
||||
for (const depField of ["dependencies", "devDependencies"]) {
|
||||
if (!pkg[depField]) continue;
|
||||
for (const [name, value] of Object.entries(pkg[depField])) {
|
||||
if (!workspaceNames.has(name)) continue;
|
||||
// Skip wildcard references (used by private apps)
|
||||
if (value === "*") continue;
|
||||
pkg[depField][name] = newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Leave peerDependencies ranges untouched
|
||||
|
||||
// Preserve original formatting (detect indent)
|
||||
const indent = raw.match(/^(\s+)"/m)?.[1] ?? " ";
|
||||
const newRaw = JSON.stringify(pkg, null, indent) + "\n";
|
||||
|
||||
return newRaw !== raw ? newRaw : null;
|
||||
}
|
||||
|
||||
const CHANGELOG_REPO_URL = "https://github.com/dicebear/dicebear";
|
||||
|
||||
/**
|
||||
* Promotes the `## [Unreleased]` section of a Keep a Changelog file to a
|
||||
* released version: the existing entries are moved under a new
|
||||
* `## [<version>] - <date>` heading, a fresh empty `## [Unreleased]` is kept on
|
||||
* top, and the bottom link references are updated accordingly.
|
||||
*
|
||||
* @param {string} raw - Raw CHANGELOG.md content
|
||||
* @param {string} newVersion
|
||||
* @param {string} date - Release date in `YYYY-MM-DD` form
|
||||
* @returns {string | null} Updated changelog, or null if there is nothing to do
|
||||
*/
|
||||
export function updateChangelog(raw, newVersion, date) {
|
||||
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
||||
|
||||
if (!unreleasedHeading.test(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Idempotent: if this version was already promoted (e.g. a previous release
|
||||
// run failed after writing the changelog), don't promote the now-empty
|
||||
// Unreleased section again. The closing bracket prevents `10.0.1` from
|
||||
// matching `10.0.10`.
|
||||
if (raw.includes(`## [${newVersion}]`)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Move the current Unreleased entries under a dated version heading and keep
|
||||
// a fresh, empty Unreleased section on top.
|
||||
let result = raw.replace(
|
||||
unreleasedHeading,
|
||||
`## [Unreleased]\n\n## [${newVersion}] - ${date}`
|
||||
);
|
||||
|
||||
// Update the bottom link references when they follow the conventional shape.
|
||||
const unreleasedLink =
|
||||
/^\[Unreleased\]:\s*\S+\/compare\/v([\w.-]+)\.\.\.HEAD[^\n]*$/m;
|
||||
const match = result.match(unreleasedLink);
|
||||
|
||||
if (match) {
|
||||
const prevVersion = match[1];
|
||||
result = result.replace(
|
||||
unreleasedLink,
|
||||
`[Unreleased]: ${CHANGELOG_REPO_URL}/compare/v${newVersion}...HEAD\n` +
|
||||
`[${newVersion}]: ${CHANGELOG_REPO_URL}/compare/v${prevVersion}...v${newVersion}`
|
||||
);
|
||||
}
|
||||
|
||||
return result !== raw ? result : null;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Resolves workspace patterns from root package.json into absolute paths
|
||||
* to each workspace's package.json. The root package.json is NOT included.
|
||||
*
|
||||
* @param {string} rootDir - Absolute path to the monorepo root
|
||||
* @returns {string[]} Absolute paths to workspace package.json files
|
||||
*/
|
||||
export function resolveWorkspacePackages(rootDir) {
|
||||
const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8"));
|
||||
const patterns = rootPkg.workspaces?.packages ?? rootPkg.workspaces ?? [];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const starIdx = pattern.indexOf("*");
|
||||
|
||||
if (starIdx === -1) {
|
||||
// Direct path — no glob
|
||||
const pkgJson = join(rootDir, pattern, "package.json");
|
||||
if (existsSync(pkgJson)) {
|
||||
results.push(pkgJson);
|
||||
}
|
||||
} else {
|
||||
// Expand single trailing `*` via readdirSync
|
||||
const base = pattern.slice(0, starIdx);
|
||||
const baseDir = join(rootDir, base);
|
||||
if (!existsSync(baseDir)) continue;
|
||||
|
||||
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const pkgJson = join(baseDir, entry.name, "package.json");
|
||||
if (existsSync(pkgJson)) {
|
||||
results.push(pkgJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { shouldPublish } from "./lib/publish.mjs";
|
||||
import { resolveWorkspacePackages } from "./lib/workspace.mjs";
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, "..");
|
||||
|
||||
const distTag = process.argv[2];
|
||||
if (!distTag) {
|
||||
console.error("Usage: node scripts/publish.mjs <dist-tag>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonPaths = resolveWorkspacePackages(ROOT);
|
||||
|
||||
let published = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const pkgPath of packageJsonPaths) {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
|
||||
// Check registry version
|
||||
let registryVersion;
|
||||
try {
|
||||
registryVersion = execFileSync("npm", ["view", pkg.name, "version"], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
// Package not yet on registry
|
||||
registryVersion = null;
|
||||
}
|
||||
|
||||
const decision = shouldPublish(pkg, registryVersion);
|
||||
|
||||
if (!decision.publish) {
|
||||
console.log(` skip (${decision.reason}): ${pkg.name}${pkg.version ? "@" + pkg.version : ""}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pkgDir = join(pkgPath, "..");
|
||||
console.log(` publish: ${pkg.name}@${pkg.version} (tag: ${distTag})`);
|
||||
execFileSync("npm", ["publish", "--tag", distTag, "--ignore-scripts"], {
|
||||
cwd: pkgDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
published++;
|
||||
}
|
||||
|
||||
console.log(`\nDone! Published: ${published}, Skipped: ${skipped}`);
|
||||
@@ -0,0 +1,117 @@
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
import { isValidVersion, updatePackageJson, collectWorkspaceNames, updateChangelog } from "./lib/version.mjs";
|
||||
import { resolveWorkspacePackages } from "./lib/workspace.mjs";
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, "..");
|
||||
|
||||
const version = process.argv[2];
|
||||
if (!version) {
|
||||
console.error("Usage: node scripts/version.mjs <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isValidVersion(version)) {
|
||||
console.error(`Invalid version: ${version}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageJsonPaths = [join(ROOT, "package.json"), ...resolveWorkspacePackages(ROOT)];
|
||||
|
||||
// Collect all workspace package names
|
||||
const workspaceNames = collectWorkspaceNames(
|
||||
packageJsonPaths.map((p) => readFileSync(p, "utf-8"))
|
||||
);
|
||||
|
||||
// Update each package.json
|
||||
for (const pkgPath of packageJsonPaths) {
|
||||
const raw = readFileSync(pkgPath, "utf-8");
|
||||
const pkg = JSON.parse(raw);
|
||||
const oldVersion = pkg.version;
|
||||
const newRaw = updatePackageJson(raw, version, workspaceNames);
|
||||
|
||||
if (newRaw !== null) {
|
||||
writeFileSync(pkgPath, newRaw);
|
||||
console.log(` ${pkg.name ?? "root"}: ${oldVersion ?? "-"} → ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The Python core is not an npm workspace (like the PHP core), so bump its
|
||||
// pyproject.toml here to keep all ports on the same version. Only the version
|
||||
// string is replaced so the rest of the manifest stays byte-for-byte untouched.
|
||||
const pyprojectPath = join(ROOT, "src/python/core/pyproject.toml");
|
||||
if (existsSync(pyprojectPath)) {
|
||||
const raw = readFileSync(pyprojectPath, "utf-8");
|
||||
const updated = raw.replace(/^version = "[^"]*"$/m, `version = "${version}"`);
|
||||
|
||||
if (updated !== raw) {
|
||||
writeFileSync(pyprojectPath, updated);
|
||||
console.log(` dicebear-core (python): → ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The Rust core is not an npm workspace either; bump its Cargo.toml so it ships
|
||||
// on the same version as the other ports. Only the [package] version line (at
|
||||
// column 0) matches `^version = "…"`, not the indented dependency versions.
|
||||
const cargoPath = join(ROOT, "src/rust/core/Cargo.toml");
|
||||
if (existsSync(cargoPath)) {
|
||||
const raw = readFileSync(cargoPath, "utf-8");
|
||||
const updated = raw.replace(/^version = "[^"]*"$/m, `version = "${version}"`);
|
||||
|
||||
if (updated !== raw) {
|
||||
writeFileSync(cargoPath, updated);
|
||||
console.log(` dicebear-core (rust): → ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The Dart core is not an npm workspace either; bump its pubspec.yaml so it
|
||||
// ships on the same version as the other ports. pub.dev's automated publishing
|
||||
// requires the pubspec version to match the v{{version}} tag exactly. Only the
|
||||
// top-level `version:` line (at column 0) matches; indented dependency
|
||||
// constraints do not.
|
||||
const pubspecPath = join(ROOT, "src/dart/core/pubspec.yaml");
|
||||
if (existsSync(pubspecPath)) {
|
||||
const raw = readFileSync(pubspecPath, "utf-8");
|
||||
const updated = raw.replace(/^version: .*$/m, `version: ${version}`);
|
||||
|
||||
if (updated !== raw) {
|
||||
writeFileSync(pubspecPath, updated);
|
||||
console.log(` dicebear_core (dart): → ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The Go core (src/go/core) needs no file bump: a Go module's version lives
|
||||
// entirely in the Git tag, which the module proxy reads directly. The tag
|
||||
// created below (e.g. v10.2.0) is mirrored to the standalone dicebear-go repo by
|
||||
// split-go-core.yml, and `github.com/dicebear/dicebear-go/v10` resolves it from
|
||||
// there. The major version is encoded in the module path (/v10), so it only
|
||||
// changes by hand on a major bump — not here.
|
||||
|
||||
// Promote the changelog's Unreleased section to the new version
|
||||
const changelogPath = join(ROOT, "CHANGELOG.md");
|
||||
if (existsSync(changelogPath)) {
|
||||
const raw = readFileSync(changelogPath, "utf-8");
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const updated = updateChangelog(raw, version, date);
|
||||
|
||||
if (updated !== null) {
|
||||
writeFileSync(changelogPath, updated);
|
||||
console.log(`\nCHANGELOG.md: Unreleased → ${version} (${date})`);
|
||||
} else {
|
||||
console.log("\nCHANGELOG.md: nothing to promote (skipped)");
|
||||
}
|
||||
}
|
||||
|
||||
// Sync package-lock.json
|
||||
console.log("\nSyncing package-lock.json...");
|
||||
execSync("npm install --package-lock-only", { cwd: ROOT, stdio: "inherit" });
|
||||
|
||||
// Git commit and tag
|
||||
const tag = `v${version}`;
|
||||
console.log(`\nCreating commit and tag ${tag}...`);
|
||||
execSync("git add -A", { cwd: ROOT, stdio: "inherit" });
|
||||
execSync(`git commit -m "${tag}"`, { cwd: ROOT, stdio: "inherit" });
|
||||
execSync(`git tag "${tag}"`, { cwd: ROOT, stdio: "inherit" });
|
||||
|
||||
console.log(`\nDone! Push with: git push && git push --tags`);
|
||||
Reference in New Issue
Block a user