Files
tutti/tools/scripts/npm-release-packages.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

266 lines
6.7 KiB
JavaScript

import { access, readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const defaultWorkspaceRoot = fileURLToPath(new URL("../..", import.meta.url));
export const workspaceRoot =
process.env.TUTTI_WORKSPACE_ROOT ?? defaultWorkspaceRoot;
export async function getNpmReleasePackages({
packageNames: selectedNames = null
} = {}) {
const packageMap = await discoverWorkspacePackages();
const releasePackageNames = await readReleasePackageNames();
validateReleasePackageSelection(packageMap, releasePackageNames);
const selectedPackageNames = selectReleasePackageNames(
releasePackageNames,
selectedNames
);
return selectedPackageNames.map((name) => {
const packageConfig = packageMap.get(name);
if (!packageConfig) {
throw new Error(
`Release package ${name} from .changeset/config.json was not found under packages/*`
);
}
if (packageConfig.manifest.private !== false) {
throw new Error(
`Release package ${name} must set "private": false before joining the npm release flow`
);
}
if (packageConfig.manifest.publishConfig?.access !== "public") {
throw new Error(
`Release package ${name} must set publishConfig.access = "public"`
);
}
return packageConfig;
});
}
export function parseReleasePackageFilters(args) {
const normalizedArgs = args.filter((arg) => arg !== "--");
if (normalizedArgs.length === 0) {
return null;
}
if (normalizedArgs.length !== 2 || normalizedArgs[0] !== "--packages-json") {
throw new Error(
"release package selection accepts only --packages-json <json-array>"
);
}
let packageNames;
try {
packageNames = JSON.parse(normalizedArgs[1]);
} catch {
throw new Error("--packages-json must contain valid JSON");
}
if (
!Array.isArray(packageNames) ||
packageNames.length === 0 ||
packageNames.some((name) => typeof name !== "string" || name.length === 0)
) {
throw new Error("--packages-json must contain a non-empty string array");
}
return [...new Set(packageNames)];
}
export function selectReleasePackageNames(
releasePackageNames,
selectedPackageNames
) {
if (selectedPackageNames === null) {
return releasePackageNames;
}
const releaseNameSet = new Set(releasePackageNames);
const unknownNames = selectedPackageNames.filter(
(name) => !releaseNameSet.has(name)
);
if (unknownNames.length > 0) {
throw new Error(
`Unknown npm release package filter(s): ${unknownNames.join(", ")}`
);
}
return selectedPackageNames;
}
async function readReleasePackageNames() {
const changesetConfigText = await readFile(
join(workspaceRoot, ".changeset/config.json"),
"utf8"
);
const changesetConfig = JSON.parse(changesetConfigText);
const fixedGroups = Array.isArray(changesetConfig.fixed)
? changesetConfig.fixed
: [];
const seen = new Set();
const names = [];
for (const group of fixedGroups) {
if (!Array.isArray(group)) {
continue;
}
for (const name of group) {
if (typeof name !== "string" || seen.has(name)) {
continue;
}
seen.add(name);
names.push(name);
}
}
if (names.length === 0) {
throw new Error(
"No npm release packages are configured in .changeset/config.json"
);
}
return names;
}
export function validateReleasePackageSelection(
packageMap,
releasePackageNames
) {
const releaseNameSet = new Set(releasePackageNames);
const missingPublicPackages = [];
const invalidRuntimeDependencies = [];
for (const packageConfig of packageMap.values()) {
if (
isPublicReleaseWorkspacePackage(packageConfig.manifest) &&
!releaseNameSet.has(packageConfig.name)
) {
missingPublicPackages.push(packageConfig.name);
}
}
for (const releasePackageName of releasePackageNames) {
const packageConfig = packageMap.get(releasePackageName);
if (!packageConfig) {
continue;
}
for (const dependencyName of collectWorkspaceRuntimeDependencyNames(
packageConfig.manifest,
packageMap
)) {
if (!releaseNameSet.has(dependencyName)) {
invalidRuntimeDependencies.push(
`${packageConfig.name} -> ${dependencyName}`
);
}
}
}
if (
missingPublicPackages.length === 0 &&
invalidRuntimeDependencies.length === 0
) {
return;
}
const lines = [];
if (missingPublicPackages.length > 0) {
lines.push(
`Public workspace packages missing from .changeset/config.json fixed release group: ${missingPublicPackages.join(", ")}`
);
}
if (invalidRuntimeDependencies.length > 0) {
lines.push(
`Release packages must not depend on workspace runtime packages outside the fixed release group: ${invalidRuntimeDependencies.join(", ")}`
);
}
throw new Error(lines.join("\n"));
}
export function isPublicReleaseWorkspacePackage(manifest) {
return (
manifest.private === false && manifest.publishConfig?.access === "public"
);
}
export function collectWorkspaceRuntimeDependencyNames(manifest, packageMap) {
const dependencyNames = new Set();
for (const field of [
"dependencies",
"optionalDependencies",
"peerDependencies"
]) {
const dependencies = manifest[field];
if (!dependencies || typeof dependencies !== "object") {
continue;
}
for (const dependencyName of Object.keys(dependencies)) {
if (packageMap.has(dependencyName)) {
dependencyNames.add(dependencyName);
}
}
}
return dependencyNames;
}
async function discoverWorkspacePackages() {
const packagesRoot = join(workspaceRoot, "packages");
const groups = await readdir(packagesRoot, { withFileTypes: true });
const packageMap = new Map();
for (const group of groups) {
if (!group.isDirectory()) {
continue;
}
const groupRoot = join(packagesRoot, group.name);
const packages = await readdir(groupRoot, { withFileTypes: true });
for (const packageEntry of packages) {
if (!packageEntry.isDirectory()) {
continue;
}
const directory = join("packages", group.name, packageEntry.name);
const manifestPath = join(directory, "package.json");
try {
await access(join(workspaceRoot, manifestPath));
} catch {
continue;
}
const manifestText = await readFile(
join(workspaceRoot, manifestPath),
"utf8"
);
const manifest = JSON.parse(manifestText);
if (typeof manifest.name !== "string") {
continue;
}
packageMap.set(manifest.name, {
directory,
manifest,
manifestPath,
name: manifest.name
});
}
}
return packageMap;
}