Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions scripts/release/pack-pr-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ import {
type Package,
type PrPackagePlan,
} from "./config.ts";

type Manifest = {
name?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};

const DEPENDENCY_SECTIONS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] as const;
import {
DEPENDENCY_SECTIONS,
duplicateWorkspaceDependencies,
graphEdgeTag,
graphUrl,
type Manifest,
} from "./pr-package-graph.ts";

function rewriteDependencies(
plan: PrPackagePlan,
Expand All @@ -42,9 +34,12 @@ function rewriteDependencies(
plan.packages.map((p) => [p.name, p]),
);
const publishable = new Set(plan.publishable_names);
const duplicates = duplicateWorkspaceDependencies(plan);
const manifest = JSON.parse(
readFileSync(manifestPath, "utf-8"),
) as Manifest;
const parentName =
manifest.name ?? plan.packages.find((pkg) => pkg.dir === dir)?.name ?? dir;
let rewritten = false;

for (const section of DEPENDENCY_SECTIONS) {
Expand All @@ -59,7 +54,10 @@ function rewriteDependencies(
);
}
if (!dependency) continue;
const url = `https://${plan.install_host}/${dependency.install}/${plan.dependency_tag}`;
const tag = duplicates.has(name)
? graphEdgeTag(plan.dependency_tag, parentName)
: plan.dependency_tag;
const url = graphUrl(plan, dependency.install, tag);
dependencies[name] = url;
console.log(
` ${manifest.name ?? dir}: ${section}.${name}: ${value} → ${url}`,
Expand Down
69 changes: 69 additions & 0 deletions scripts/release/pr-package-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createHash } from "node:crypto";
import type { PrPackagePlan } from "./config.ts";

export type Manifest = {
name?: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};

export const DEPENDENCY_SECTIONS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] as const;

export function duplicateWorkspaceDependencies(
plan: PrPackagePlan,
): Set<string> {
// Every pack matrix job sees the same plan and clean checkout, so each can
// derive the graph-wide duplicate set without cross-job state.
const selected = new Set(plan.packages.map((pkg) => pkg.name));
const counts = new Map<string, number>();

for (const pkg of plan.packages) {
const manifest = JSON.parse(
readFileSync(join(pkg.dir, "package.json"), "utf8"),
) as Manifest;
for (const section of DEPENDENCY_SECTIONS) {
for (const [name, value] of Object.entries(manifest[section] ?? {})) {
if (value.startsWith("workspace:") && selected.has(name)) {
counts.set(name, (counts.get(name) ?? 0) + 1);
}
}
}
}

return new Set(
[...counts].filter(([, count]) => count > 1).map(([name]) => name),
);
}

export function graphEdgeTag(
dependencyTag: string,
parentName: string,
): string {
// Keep the readable basename short, but hash the full package name so
// parents with the same basename in different npm scopes stay distinct.
// Bun uses the tag in a package-store file name with a 255-byte limit.
const parent = parentName.replace(/^@[^/]+\//, "");
const hash = createHash("sha256")
.update(parentName)
.digest("hex")
.slice(0, 8);
const identity = `${parent.slice(0, 80)}-${hash}`;
return `${dependencyTag}-from-${identity}`;
}

export function graphUrl(
plan: PrPackagePlan,
install: string,
tag: string,
): string {
return `https://${plan.install_host}/${install}/${encodeURIComponent(tag)}`;
}
43 changes: 42 additions & 1 deletion scripts/release/publish-pr-package-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
type Package,
type PrPackagePlan,
} from "./config.ts";
import {
DEPENDENCY_SECTIONS,
graphEdgeTag,
graphUrl,
type Manifest,
} from "./pr-package-graph.ts";

function findTarball(artifactRoot: string, pkg: Package): string {
const artifactDir = join(artifactRoot, pkg.artifact);
Expand All @@ -26,6 +32,21 @@ function findTarball(artifactRoot: string, pkg: Package): string {
return tarballs[0]!;
}

function packedManifest(tarball: string): Manifest {
const result = Bun.spawnSync(
["tar", "-xOf", tarball, "package/package.json"],
{
stdout: "pipe",
stderr: "pipe",
},
);
if (result.exitCode !== 0) {
process.stderr.write(result.stderr);
fail(`Failed to read package/package.json from ${tarball}`);
}
return JSON.parse(result.stdout.toString()) as Manifest;
}

async function upload(
host: string,
token: string,
Expand Down Expand Up @@ -77,10 +98,30 @@ const entries = plan.packages.map((pkg) => ({
pkg,
tarball: findTarball(artifactRoot, pkg),
}));
const byName = new Map(entries.map((entry) => [entry.pkg.name, entry]));
const dependencyTags = new Map(
entries.map(({ pkg }) => [pkg.name, new Set([plan.dependency_tag])]),
);

for (const { pkg, tarball } of entries) {
const manifest = packedManifest(tarball);
for (const section of DEPENDENCY_SECTIONS) {
for (const [name, value] of Object.entries(manifest[section] ?? {})) {
const dependency = byName.get(name)?.pkg;
if (!dependency) continue;
const tag = graphEdgeTag(plan.dependency_tag, manifest.name ?? pkg.name);
if (value === graphUrl(plan, dependency.install, tag)) {
dependencyTags.get(name)!.add(tag);
}
}
}
}

console.log("Publishing same-commit dependency graph");
for (const { pkg, tarball } of entries) {
await upload(host, token, ttl, pkg, tarball, [plan.dependency_tag]);
await upload(host, token, ttl, pkg, tarball, [
...dependencyTags.get(pkg.name)!,
]);
}

console.log("Dependency graph complete; exposing public tags");
Expand Down