Emdash source with visual editor image upload fix

Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
2026-05-03 10:44:54 +07:00
parent 78f81bebb6
commit 2d1be52177
2352 changed files with 662964 additions and 0 deletions

339
scripts/sync-templates-repo.mjs Executable file
View File

@@ -0,0 +1,339 @@
#!/usr/bin/env node
/**
* Sync templates from this monorepo to the standalone emdash-cms/templates repo.
*
* - Clones emdash-cms/templates to a temp directory
* - Copies each template, excluding build artifacts
* - Resolves workspace:* and catalog: versions to real published versions
* - Commits and pushes a branch, then opens a PR
*
* Usage:
* node scripts/sync-templates-repo.mjs # full run: clone, sync, PR
* node scripts/sync-templates-repo.mjs --dry-run # sync to temp dir, print diff, don't push
* node scripts/sync-templates-repo.mjs --local /path/to/repo # sync to a local checkout
*/
import { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readlinkSync,
readdirSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const TEMPLATES_DIR = join(ROOT, "templates");
const REPO = "emdash-cms/templates";
const TEMPLATES = [
"blog",
"blog-cloudflare",
"marketing",
"marketing-cloudflare",
"portfolio",
"portfolio-cloudflare",
"starter",
"starter-cloudflare",
];
const EXCLUDE = new Set(["node_modules", "dist", ".astro", ".emdash", "CHANGELOG.md"]);
const RE_NON_WHITESPACE_START = /^\S/;
const RE_CATALOG_ENTRY = /^\s+"?([^"]+)"?:\s+(.+)$/;
function parseCatalog() {
const yaml = readFileSync(join(ROOT, "pnpm-workspace.yaml"), "utf8");
const catalog = {};
let inCatalog = false;
for (const line of yaml.split("\n")) {
if (line.startsWith("catalog:")) {
inCatalog = true;
continue;
}
if (inCatalog && RE_NON_WHITESPACE_START.test(line)) break;
if (!inCatalog) continue;
const match = line.match(RE_CATALOG_ENTRY);
if (match) catalog[match[1]] = match[2];
}
return catalog;
}
function collectWorkspaceVersions() {
const versions = {};
const dirs = [join(ROOT, "packages"), join(ROOT, "packages/plugins")];
for (const base of dirs) {
if (!existsSync(base)) continue;
for (const entry of readdirSync(base, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const pkgPath = join(base, entry.name, "package.json");
if (!existsSync(pkgPath)) continue;
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
if (pkg.name && pkg.version) {
versions[pkg.name] = pkg.version;
}
}
}
return versions;
}
function resolveDeps(deps, catalog, workspace) {
if (!deps) return deps;
const resolved = {};
for (const [name, version] of Object.entries(deps)) {
if (version === "catalog:") {
resolved[name] = catalog[name] || version;
} else if (version.startsWith("workspace:")) {
resolved[name] = workspace[name] ? `^${workspace[name]}` : version;
} else {
resolved[name] = version;
}
}
return resolved;
}
function transformPackageJson(srcPath, catalog, workspace) {
const pkg = JSON.parse(readFileSync(srcPath, "utf8"));
pkg.dependencies = resolveDeps(pkg.dependencies, catalog, workspace);
pkg.devDependencies = resolveDeps(pkg.devDependencies, catalog, workspace);
if (pkg.peerDependencies) {
pkg.peerDependencies = resolveDeps(pkg.peerDependencies, catalog, workspace);
if (Object.keys(pkg.peerDependencies).length === 0) delete pkg.peerDependencies;
}
if (pkg.optionalDependencies) {
pkg.optionalDependencies = resolveDeps(pkg.optionalDependencies, catalog, workspace);
if (Object.keys(pkg.optionalDependencies).length === 0) delete pkg.optionalDependencies;
}
return JSON.stringify(pkg, null, "\t") + "\n";
}
function lexists(p) {
try {
lstatSync(p);
return true;
} catch {
return false;
}
}
function copyDirRecursive(src, dest) {
mkdirSync(dest, { recursive: true });
for (const entry of readdirSync(src, { withFileTypes: true })) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isSymbolicLink()) {
if (lexists(destPath)) unlinkSync(destPath);
symlinkSync(readlinkSync(srcPath), destPath);
} else if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else {
cpSync(srcPath, destPath);
}
}
}
function copyTemplateDir(src, dest) {
mkdirSync(dest, { recursive: true });
for (const entry of readdirSync(src, { withFileTypes: true })) {
if (EXCLUDE.has(entry.name)) continue;
// Don't overwrite target-only files
if (entry.name === "README.md") continue;
// package.json is handled separately
if (entry.name === "package.json") continue;
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isSymbolicLink()) {
if (lexists(destPath)) unlinkSync(destPath);
symlinkSync(readlinkSync(srcPath), destPath);
} else if (entry.isDirectory()) {
if (existsSync(destPath)) rmSync(destPath, { recursive: true });
copyDirRecursive(srcPath, destPath);
} else {
cpSync(srcPath, destPath);
}
}
// Remove files in dest that don't exist in src (except preserved ones)
const preserved = new Set(["README.md", "package.json"]);
for (const entry of readdirSync(dest, { withFileTypes: true })) {
if (preserved.has(entry.name)) continue;
if (EXCLUDE.has(entry.name)) continue;
const srcPath = join(src, entry.name);
if (!existsSync(srcPath)) {
const destPath = join(dest, entry.name);
rmSync(destPath, { recursive: true, force: true });
}
}
}
function git(args, cwd) {
return execFileSync("git", args, { encoding: "utf8", stdio: "pipe", cwd }).trim();
}
// --- main ---
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const localIdx = args.indexOf("--local");
const localPath = localIdx !== -1 ? args[localIdx + 1] : null;
if (localIdx !== -1 && !localPath) {
console.error("Error: --local requires a path argument");
process.exit(1);
}
const catalog = parseCatalog();
const workspace = collectWorkspaceVersions();
console.log("Workspace packages:");
for (const [name, version] of Object.entries(workspace)) {
console.log(` ${name} = ${String(version)}`);
}
console.log("");
let targetDir;
let tempDir;
if (localPath) {
targetDir = resolve(localPath);
if (!existsSync(join(targetDir, ".git"))) {
console.error(`Error: ${targetDir} is not a git repository`);
process.exit(1);
}
} else {
tempDir = mkdtempSync(join(tmpdir(), "emdash-templates-"));
console.log(`Cloning ${REPO} to ${tempDir}...`);
execFileSync("gh", ["repo", "clone", REPO, tempDir, "--", "--depth", "1"], {
stdio: "pipe",
});
// Configure git credential helper so push works with GH_TOKEN
execFileSync("gh", ["auth", "setup-git"], { stdio: "pipe" });
targetDir = tempDir;
}
try {
for (const template of TEMPLATES) {
const srcDir = join(TEMPLATES_DIR, template);
const destDir = join(targetDir, template);
if (!existsSync(srcDir)) {
console.log(`Skipping ${template} (not in monorepo)`);
continue;
}
console.log(`Syncing ${template}`);
copyTemplateDir(srcDir, destDir);
const srcPkg = join(srcDir, "package.json");
if (existsSync(srcPkg)) {
writeFileSync(
join(destDir, "package.json"),
transformPackageJson(srcPkg, catalog, workspace),
);
console.log(" Transformed package.json");
}
}
// Also sync screenshots.json
const screenshotsJson = join(TEMPLATES_DIR, "screenshots.json");
if (existsSync(screenshotsJson)) {
cpSync(screenshotsJson, join(targetDir, "screenshots.json"));
console.log("\nSynced screenshots.json");
}
console.log("");
const diff = git(["diff", "--stat"], targetDir);
const untracked = git(["ls-files", "--others", "--exclude-standard"], targetDir);
if (!diff && !untracked) {
console.log("No changes to sync.");
process.exit(0);
}
console.log("Changes:");
console.log(diff);
console.log("");
if (dryRun) {
console.log("Dry run — not pushing.");
if (tempDir) {
console.log(`Temp dir preserved at: ${tempDir}`);
tempDir = undefined; // preserve for inspection
}
process.exit(0);
}
// Get the emdash version for the branch/commit message
const emdashVersion = workspace["emdash"] || "unknown";
const branch = `sync/emdash-v${emdashVersion}`;
// Configure git identity in CI
if (process.env.CI) {
git(["config", "user.name", "github-actions[bot]"], targetDir);
git(["config", "user.email", "github-actions[bot]@users.noreply.github.com"], targetDir);
}
git(["checkout", "-B", branch], targetDir);
git(["add", "-A"], targetDir);
git(["commit", "-m", `chore: sync templates from emdash v${emdashVersion}`], targetDir);
git(["push", "--force", "-u", "origin", branch], targetDir);
console.log(`Pushed branch: ${branch}`);
const prBody = [
"## Summary",
"",
`Synced templates from [emdash v${emdashVersion}](https://github.com/emdash-cms/emdash).`,
"",
"Auto-generated by `scripts/sync-templates-repo.mjs`.",
].join("\n");
// Reuse existing PR if one is already open for this branch
const existingPrs = JSON.parse(
execFileSync(
"gh",
["pr", "list", "--repo", REPO, "--head", branch, "--state", "open", "--json", "url"],
{ encoding: "utf8", stdio: "pipe", cwd: targetDir },
),
);
if (existingPrs.length > 0) {
console.log(`PR already exists: ${existingPrs[0].url}`);
} else {
const prUrl = execFileSync(
"gh",
[
"pr",
"create",
"--repo",
REPO,
"--head",
branch,
"--title",
`chore: sync templates from emdash v${emdashVersion}`,
"--body",
prBody,
],
{ encoding: "utf8", stdio: "pipe", cwd: targetDir },
).trim();
console.log(`PR: ${prUrl}`);
}
} finally {
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
}