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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ For workspaces and monorepos:
pnpm exec pkg-pr-new publish './packages/A' './packages/B' # or `pnpm exec pkg-pr-new publish './packages/*'`
```

You can also pass **prebuilt tarballs** (`.tgz` or `.tar.gz`) directly, which is handy when your build pipeline already produces tarballs in a custom way:

```sh
pnpm exec pkg-pr-new publish './artifacts/*.tgz'
```

> [!NOTE]
> Prebuilt tarballs are uploaded **as-is**: pkg.pr.new will not repack them. If one tarball references another tarball being published in the same call, pkg.pr.new will print a warning and that reference will not be rewritten to a pkg.pr.new URL. Repack with the resolved version yourself if you need cross-package linking.

> [!CAUTION]
> In CI environments, avoid `npx`, `pnpm dlx`, `yarn dlx`, and `bunx` for this step. Install `pkg-pr-new` as a dependency and execute it from the lockfile (`npm exec`, `pnpm exec`, `yarn`, or `bun run`).

Expand Down
162 changes: 160 additions & 2 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
installCommands,
} from "@pkg-pr-new/utils";
import { glob } from "tinyglobby";
import { parseTarGzip, type ParsedTarFileItem } from "nanotar";
import ignore from "ignore";
import "./environments";
import { isBinaryFile } from "isbinaryfile";
Expand Down Expand Up @@ -123,15 +124,48 @@ const main = defineCommand({
},
},
run: async ({ args }) => {
const paths =
const rawInputs =
args._.length > 0
? await glob(args._, {
expandDirectories: false,
onlyDirectories: true,
onlyFiles: false,
absolute: true,
})
: [process.cwd()];

const paths: string[] = [];
const tarballPaths: string[] = [];
for (const input of rawInputs) {
let stat;
try {
stat = await fs.stat(input);
} catch {
console.warn(`Skipping ${input}: cannot stat`);
continue;
}
if (stat.isDirectory()) {
paths.push(input);
} else if (
stat.isFile() &&
(input.endsWith(".tgz") || input.endsWith(".tar.gz"))
) {
tarballPaths.push(input);
} else {
console.warn(
`Skipping ${input}: not a directory or .tgz/.tar.gz file`,
);
}
}

if (paths.length > 0 && tarballPaths.length > 0) {
console.error(
"pkg-pr-new: cannot mix directory and prebuilt tarball inputs in the same publish.",
);
process.exit(1);
}

const isTarballMode = tarballPaths.length > 0;

const templates = await glob(args.template || [], {
expandDirectories: false,
onlyDirectories: true,
Expand Down Expand Up @@ -268,6 +302,7 @@ const main = defineCommand({
const packageInfos: Array<{
packageName: string;
pJson: PackageJson;
tarballPath?: string;
}> = [];

for (const p of paths) {
Expand All @@ -286,6 +321,82 @@ const main = defineCommand({
packageInfos.push({ packageName, pJson });
}

for (const tgzPath of tarballPaths) {
let pJson: PackageJson | null;
try {
pJson = await readPackageJsonFromTarball(tgzPath);
} catch (error) {
console.warn(
`Skipping ${tgzPath}: ${error instanceof Error ? error.message : String(error)}`,
);
continue;
}

if (!pJson) {
console.warn(
`Skipping ${tgzPath}: no top-level package.json found inside the tarball`,
);
continue;
}

if (pJson.private) {
console.warn(
`Skipping ${tgzPath}: the package is marked private`,
);
continue;
}

if (!pJson.name) {
throw new Error(
`"name" field in the package.json inside ${tgzPath} should be defined`,
);
}

packageInfos.push({
packageName: pJson.name,
pJson,
tarballPath: tgzPath,
});
}

if (isTarballMode && packageInfos.length > 1) {
const allPackageNames = new Set(
packageInfos.map((info) => info.packageName),
);
const depFields = [
"dependencies",
"devDependencies",
"optionalDependencies",
...(isPeerDepsEnabled ? (["peerDependencies"] as const) : []),
] as const;

for (const info of packageInfos) {
const siblings = new Set<string>();
for (const field of depFields) {
const deps = info.pJson[field];
if (!deps) {
continue;
}
for (const depName of Object.keys(deps)) {
if (
allPackageNames.has(depName) &&
depName !== info.packageName
) {
siblings.add(depName);
}
}
}
if (siblings.size > 0) {
const list = [...siblings].map((s) => `'${s}'`).join(", ");
console.warn(
`warning: prebuilt tarball '${info.tarballPath}' references sibling package(s) ${list} in its package.json. ` +
`Those references will NOT be rewritten to pkg.pr.new URLs because the tarball is taken as-is. ` +
`Pass the source directory instead, or repack after replacing those versions manually if you need cross-package linking.`,
);
}
}
}

if (isCompact) {
for (const { packageName } of packageInfos) {
try {
Expand Down Expand Up @@ -505,6 +616,27 @@ const main = defineCommand({
}
}

for (const info of packageInfos) {
if (!info.tarballPath) {
continue;
}
const filename = path.basename(info.tarballPath);
const buffer = await fs.readFile(info.tarballPath);
const shasum = createHash("sha1").update(buffer).digest("hex");

shasums[info.packageName] = shasum;

const outputPkg = outputMetadata.packages.find(
(p) => p.name === info.packageName,
)!;
outputPkg.shasum = shasum;

const blob = new Blob([buffer], {
type: "application/octet-stream",
});
formData.append(`package:${info.packageName}`, blob, filename);
}

const formDataPackagesSize = [...formData.entries()].reduce(
(prev, [_, entry]) => prev + getFormEntrySize(entry),
0,
Expand Down Expand Up @@ -840,3 +972,29 @@ function parsePackageJson(contents: string) {
return null;
}
}

async function readPackageJsonFromTarball(
tarballPath: string,
): Promise<PackageJson | null> {
const compressed = await fs.readFile(tarballPath);

let entries: ParsedTarFileItem[];
try {
entries = await parseTarGzip(compressed, {
filter: (file) => {
const segments = file.name.split("/");
return segments.length === 2 && segments[1] === "package.json";
},
});
} catch (error) {
throw new Error(
`failed to read tarball (${error instanceof Error ? error.message : String(error)})`,
);
}

const entry = entries[0];
if (!entry) {
return null;
}
return parsePackageJson(entry.text);
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@actions/core": "^3.0.1",
"@jsdevtools/ez-spawn": "^3.0.4",
"@pkg-pr-new/utils": "workspace:^",
"citty": "^0.1.6",
"ignore": "^7.0.5",
"isbinaryfile": "5.0.2",
"nanotar": "^0.3.0",
"ohash": "^1.1.4",
"pkg-types": "^2.3.1",
"query-registry": "^4.3.0",
Expand Down
Loading
Loading