Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f10ff0f
fix: remove dotenv-cli dependency and related config
stephansama Jan 7, 2026
8998467
feat(multipublish): add new package for publishing to multiple providers
stephansama Jan 8, 2026
30e4950
feat: implement npm publishing strategies
stephansama Jan 8, 2026
97ded53
feat(multipublish): initial release of the multipublish cli tool
stephansama Jan 8, 2026
718e3b1
feat: add dry run publishing mode
stephansama Jan 8, 2026
0eccd20
chore(multipublish): narrow lint script scope to src directory
stephansama Jan 8, 2026
4fb6348
fix: update jsr config file path and npmrc handling in publish logic
stephansama Jan 8, 2026
f2af774
refactor: use getargs instead of parseargs and update changeset type …
stephansama Jan 8, 2026
6b1ac0e
fix: update package.json dependencies and fix util.ts error handling
stephansama Jan 8, 2026
9f58585
build: move zod to dependencies
stephansama Jan 8, 2026
f2ea1be
feat: add include/exclude options for jsr and --no-git-checks flag
stephansama Jan 8, 2026
918dba2
feat(multipublish): add option to allow slow types for jsr publish
stephansama Jan 8, 2026
06c9938
feat: add dependency catalog support and cli config option
stephansama Jan 9, 2026
445c042
build(multipublish): clean package.json after jsr publish with experi…
stephansama Jan 9, 2026
d030c4d
docs: add detailed usage and configuration guide
stephansama Jan 9, 2026
ee65ad7
feat: add jsr configuration options and update publish logic
stephansama Jan 9, 2026
e680b7e
fix(multipublish): correctly handle dry run argument and package vers…
stephansama Jan 9, 2026
591203b
feat: add bun support and automate changeset status generation
stephansama Jan 10, 2026
2897efd
test: add unit tests for core multipublish modules
stephansama Jan 14, 2026
b5118bf
refactor: normalize import paths in test files
stephansama Jan 14, 2026
81a90cf
test: add comprehensive tests for multipublish package publishing
stephansama Jan 15, 2026
1f30a1c
refactor(multipublish): reorder imports in catalog tests
stephansama Jan 15, 2026
82d0ecf
feat: introduce `commitJsrVersionUpdate` option for JSR publishing
stephansama Jan 15, 2026
4fe48a5
test(multipublish/util): add tests for directory, file, and stdin uti…
stephansama Jan 15, 2026
d398211
test(multipublish): add unit tests for run function
stephansama Jan 15, 2026
1cc5dbd
chore: integrate multipublish for multi-platform package releases
stephansama Jan 15, 2026
fff59de
feat(jsr): allow string import/require in export maps
stephansama Jan 15, 2026
3a61577
build(multipublish): configure multipublish cli for use
stephansama Jan 15, 2026
37623f0
refactor: remove commitJsrVersionUpdate option
stephansama Jan 15, 2026
57b983e
docs: enhance jsr and changeset documentation in multipublish readme
stephansama Jan 15, 2026
cadd823
refactor: improve workflow step name clarity
stephansama Jan 15, 2026
0991446
chore: switch npm registry to github packages
stephansama Jan 15, 2026
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
1 change: 1 addition & 0 deletions .config/.cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"kaomojis",
"macchiato",
"manypkg",
"multipublish",
"nodemon",
"nvim",
"nvmrc",
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
.env**
.next
.react-router
.publish
.svelte-kit
.turbo
.wrangler
core/ai-commit-msg/config
core/auto-readme/config
core/example/scripts/examples.json
core/multipublish/config
dist
dist-js
examples/catppuccin-xsl/vanilla/public/**/*
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ All packages are packaged underneath the `@stephansama` scope (for example: `@st
| [create-stephansama-example](core/example/README.md) | ![npm version image](https://img.shields.io/npm/v/create-stephansama-example?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/create-stephansama-example?labelColor=211F1F) | Download an example from the @stephansama/packages examples |
| [find-makefile-targets](core/find-makefile-targets/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Ffind-makefile-targets?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/find-makefile-targets?labelColor=211F1F) | Find makefile targets used to pipe into fzf |
| [github-env](core/github-env/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fgithub-env?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/github-env?labelColor=211F1F) | Additional environment variable types for GitHub CI |
| [multipublish](core/multipublish/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fmultipublish?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/multipublish?labelColor=211F1F) | Publish packages to multiple providers easily |
| [prettier-plugin-handlebars](core/prettier-plugin-handlebars/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fprettier-plugin-handlebars?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/prettier-plugin-handlebars?labelColor=211F1F) | Prettier plugin that automatically assigns the default parser for various handlebars files |
| [remark-asciinema](core/remark-asciinema/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fremark-asciinema?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/remark-asciinema?labelColor=211F1F) | A remark plugin that transforms Asciinema links into embedded players or screenshots. |
| [svelte-social-share-links](core/svelte-social-share-links/README.md) | ![npm version image](https://img.shields.io/npm/v/%40stephansama%2Fsvelte-social-share-links?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F) | ![npm downloads](https://img.shields.io/npm/dw/@stephansama/svelte-social-share-links?labelColor=211F1F) | Svelte/Web component to share the current url with various social media providers |
Expand Down
25 changes: 25 additions & 0 deletions core/multipublish/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# @stephansama/multipublish

[![Source code](https://img.shields.io/badge/Source-666666?style=flat&logo=github&label=Github&labelColor=211F1F)](https://github.com/stephansama/packages/tree/main/core/multipublish)
[![Documentation](https://img.shields.io/badge/Documentation-211F1F?style=flat&logo=Wikibooks&labelColor=211F1F)](https://packages.stephansama.info/api/@stephansama/multipublish)
[![NPM Version](https://img.shields.io/npm/v/%40stephansama%2Fmultipublish?logo=npm&logoColor=red&color=211F1F&labelColor=211F1F)](https://www.npmjs.com/package/@stephansama/multipublish)
[![npm downloads](https://img.shields.io/npm/dw/@stephansama/multipublish?labelColor=211F1F)](https://www.npmjs.com/package/@stephansama/multipublish)

Publish packages to multiple providers easily

##### Table of contents
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<details><summary>Open Table of contents</summary>

- [Installation](#installation)
- [Usage](#usage)

</details>

## Installation

```sh
pnpm install @stephansama/multipublish
```

## Usage

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add usage documentation for the CLI tool.

The Usage section is currently empty. Please document the basic commands, configuration options, and examples for users to understand how to use this multipublish CLI tool.

Would you like me to help draft initial usage documentation based on the CLI implementation?

🤖 Prompt for AI Agents
In @core/multipublish/README.md at line 25, The README's "Usage" section is
empty; add a concise usage guide that describes the CLI entrypoint and primary
commands, e.g., list commands like "multipublish init", "multipublish publish",
"multipublish status" (or the actual command names implemented in the CLI), list
common flags/options (e.g., --config, --dry-run, --verbose, --target), document
configuration file format and relevant environment variables, and include 2–3
short examples showing typical invocation sequences (init -> configure ->
publish and a dry-run example). Keep the section organized with subheadings
"Commands", "Options", "Configuration", and "Examples" and ensure each command
references the actual function/command names implemented in the codebase so
users can map docs to the CLI implementation.

32 changes: 32 additions & 0 deletions core/multipublish/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as fsp from "node:fs/promises";
import * as path from "path";
import { build as tsdown } from "tsdown";
import * as z from "zod";

const outDir = path.resolve("./dist");
const schemaDir = path.resolve("./config");

await build({ attw: false, entry: ["./src/index.ts"] });

await build({ dts: true, entry: ["./src/schema.ts"], outDir: schemaDir });

const { configSchema } = await import("./config/schema.js");

const jsonSchema = z.toJSONSchema(configSchema);

const jsonString = JSON.stringify(jsonSchema);

await fsp.writeFile(path.join(schemaDir, "schema.json"), jsonString);

/** @param {import('tsdown').Options} opts */
function build(opts) {
return tsdown({
attw: { excludeEntrypoints: ["schema.json"] },
exports: true,
format: ["esm", "cjs"],
outDir,
skipNodeModulesBundle: true,
target: "esnext",
...opts,
});
}
5 changes: 5 additions & 0 deletions core/multipublish/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

"use strict";

import("./dist/index.js").then((mod) => mod.run());
Comment on lines +1 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding error handling for import/run failures.

If the build hasn't been run or run() throws, users will see an uncaught promise rejection without helpful context. Consider catching and reporting errors gracefully:

♻️ Suggested improvement
 #!/usr/bin/env node
 
 "use strict";
 
-import("./dist/index.js").then((mod) => mod.run());
+import("./dist/index.js")
+	.then((mod) => mod.run())
+	.catch((err) => {
+		console.error("multipublish failed:", err.message);
+		process.exitCode = 1;
+	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#!/usr/bin/env node
"use strict";
import("./dist/index.js").then((mod) => mod.run());
#!/usr/bin/env node
"use strict";
import("./dist/index.js")
.then((mod) => mod.run())
.catch((err) => {
console.error("multipublish failed:", err.message);
process.exitCode = 1;
});
🤖 Prompt for AI Agents
In `@core/multipublish/cli.mjs` around lines 1 - 5, The bootstrap script currently
imports "./dist/index.js" and calls mod.run() without handling failures, so add
a rejection handler to the dynamic import and the run invocation: wrap
import("./dist/index.js") promise chain with .catch to log a clear error
(including the error object) and exit non‑zero, and also ensure any
thrown/rejected value from mod.run() is caught and reported (either by
awaiting/then/catch on mod.run() or handling its returned promise), referencing
the import("./dist/index.js") call and mod.run() invocation.

67 changes: 67 additions & 0 deletions core/multipublish/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@stephansama/multipublish",
"version": "0.0.0",
"description": "Publish packages to multiple providers easily",
"keywords": [
"multipublish"
],
"homepage": "https://packages.stephansama.info/api/@stephansama/multipublish",
"repository": {
"type": "git",
"url": "git+https://github.com/stephansama/packages.git",
"directory": "core/multipublish"
},
"license": "MIT",
"author": {
"name": "Stephan Randle",
"email": "stephanrandle.dev@gmail.com",
"url": "https://stephansama.info"
},
"type": "module",
"exports": {
".": {
"import": "./config/schema.js",
"require": "./config/schema.cjs"
},
"./package.json": "./package.json"
},
"main": "./config/schema.cjs",
"module": "./config/schema.js",
"types": "./config/schema.d.cts",
Comment thread
stephansama marked this conversation as resolved.
Comment thread
stephansama marked this conversation as resolved.
"bin": "./cli.mjs",
"files": [
"./dist",
"./config",
"./cli.mjs"
Comment on lines +33 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider removing the ./ prefix from files array entries.

While the ./ prefix is valid, it's more idiomatic to omit it in the files array. Standard practice uses relative paths without the prefix.

♻️ Proposed refactor
 	"files": [
-		"./dist",
-		"./config",
-		"./cli.mjs"
+		"dist",
+		"config",
+		"cli.mjs"
 	],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"./dist",
"./config",
"./cli.mjs"
"dist",
"config",
"cli.mjs"
🤖 Prompt for AI Agents
In @core/multipublish/package.json around lines 33 - 35, The files array entries
in package.json currently use a leading "./" (e.g., "./dist", "./config",
"./cli.mjs"); update those entries to the idiomatic form without the "./" prefix
(dist, config, cli.mjs) by editing the files array in
core/multipublish/package.json so each entry is the relative path without the
leading dot-slash.

],
Comment on lines +32 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine build configuration and package.json exports
cd core/multipublish || exit 1

echo "=== package.json exports and files ==="
jq '.exports, .files, .bin' package.json

echo ""
echo "=== build.mjs contents ==="
if [ -f build.mjs ]; then
  cat build.mjs
else
  echo "build.mjs not found"
fi

echo ""
echo "=== Directory listing ==="
ls -la

Repository: stephansama/packages

Length of output: 1723


Remove ./dist from the files array or add corresponding exports.

The dist directory is built by build.mjs (compiling src/index.ts), but the exports configuration doesn't reference it. Either remove ./dist from files if it's not intended to be published, or add exports that point to the built files in dist.

🤖 Prompt for AI Agents
In @core/multipublish/package.json around lines 32 - 36, The package.json
"files" array currently includes "./dist" but package.exports doesn’t expose
built artifacts; either remove "./dist" from the "files" array if you don’t
intend to publish built output, or add proper entries in "exports" that point to
the compiled outputs in "./dist" (e.g., map the package entrypoints used by
build.mjs / src/index.ts to their corresponding files in dist and ensure
"main"/"module"/"types" or "exports" entries reference those dist paths) so
published packages include and resolve the built files.

"scripts": {
"build": "node build.mjs",
"detect": "node ./test.js",
Comment thread
stephansama marked this conversation as resolved.
Outdated
"dev": "node --watch build.mjs",
"lint": "eslint ./ --pass-on-no-patterns --no-error-on-unmatched-pattern"
},
"dependencies": {
"@manypkg/find-root": "catalog:",
"@manypkg/get-packages": "catalog:",
"cosmiconfig": "catalog:cli",
"dedent": "^1.7.1",
"obug": "catalog:cli",
"package-manager-detector": "^1.6.0",
"yaml": "^2.8.2",
"yargs": "catalog:cli"
},
"devDependencies": {
"@types/yargs": "catalog:",
"jsr": "catalog:",
"tsdown": "catalog:",
"zod": "catalog:schema"
Comment thread
stephansama marked this conversation as resolved.
Outdated
},
"peerDependencies": {
"jsr": ">=0"
},
Comment thread
stephansama marked this conversation as resolved.
"packageManager": "pnpm@10.11.0",
"publishConfig": {
"access": "public",
"registry": "http://localhost:487"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
31 changes: 31 additions & 0 deletions core/multipublish/src/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { enable } from "obug";
import yargs, { type Options } from "yargs";
import { hideBin } from "yargs/helpers";

import { MODULE_NAME } from "./util";

const args = {
config: { alias: "c", description: "Path to config file", type: "string" },
output: { alias: "s", description: "use changesets", type: "boolean" },
Comment thread
stephansama marked this conversation as resolved.
Outdated
verbose: {
alias: "v",
description: "Enable verbose logging",
type: "boolean",
},
Comment on lines +29 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Potential alias conflict: -v is commonly used for --version.

The -v alias is assigned to --verbose, but many CLI tools use -v for --version. Consider using a different alias like -V for verbose to avoid user confusion.

🤖 Prompt for AI Agents
In @core/multipublish/src/args.ts around lines 29 - 33, The CLI option
definition for verbose (the verbose option in core/multipublish/src/args.ts)
uses alias "v" which commonly conflicts with "-v" for version; change the alias
to a less-conflicting alternative (e.g., "V" or remove the short alias) in the
verbose option definition so the alias no longer collides with any --version
handling, and update any documentation or tests that reference the old alias.

} satisfies Record<string, Options>;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export async function parseArgs() {
const yargsInstance = yargs(hideBin(process.argv))
.options(args)
.help("h")
.alias("h", "help")
.epilogue(`--> @stephansama open-source ${new Date().getFullYear()}`);

const parsed = await yargsInstance
.wrap(yargsInstance.terminalWidth())
.parse();

if (parsed.verbose) enable(`${MODULE_NAME}*`);

return parsed;
}
30 changes: 30 additions & 0 deletions core/multipublish/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { cosmiconfig, getDefaultSearchPlaces, type Options } from "cosmiconfig";

import { type Config, configSchema } from "./schema";
import { MODULE_NAME } from "./util";

const searchPlaces = getSearchPlaces();

const defaultConfig = {
platforms: [["jsr", { experimentalGenerateJSR: true }]],
} satisfies Config;

export async function loadConfig() {
const opts: Partial<Options> = { searchPlaces };

const explorer = cosmiconfig(MODULE_NAME, opts);

const result = await explorer.search();

return configSchema.parse(result?.config || defaultConfig);
}

function getSearchPlaces() {
return [
...getDefaultSearchPlaces(MODULE_NAME),
`.config/.${MODULE_NAME}rc.json`,
`.config/.${MODULE_NAME}rc.yaml`,
`.config/.${MODULE_NAME}rc.yml`,
`.config/.${MODULE_NAME}rc`,
];
}
17 changes: 17 additions & 0 deletions core/multipublish/src/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { detect } from "package-manager-detector/detect";

export type AgentName = NonNullable<Awaited<ReturnType<typeof detect>>>["name"];

let _detected: AgentName | null = null;

export async function detectPackageManager() {
if (_detected) return _detected;

const detected = await detect();
if (!detected) throw new Error("unable to detect package manager");

_detected = detected.name;
if (_detected === "bun") throw new Error("bun is not supported");

return _detected;
}
42 changes: 42 additions & 0 deletions core/multipublish/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env node
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import { findRoot } from "@manypkg/find-root";
import { getPackages } from "@manypkg/get-packages";

import { parseArgs } from "./args";
import { loadConfig } from "./config";
import { publishPlatform } from "./publish";
import * as util from "./util";

export async function run() {
const args = await parseArgs();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
stephansama marked this conversation as resolved.
Outdated
const config = await loadConfig();

const root = await findRoot(process.cwd());
const { packages } = await getPackages(root.rootDir);
const { releases } = await util.getChangesetReleases();

const releasedPackages = releases.map((release) => {
const packageJson = packages.find(
(pkg) => pkg.packageJson.name === release.name,
);

if (!packageJson) {
throw new Error(
`unable to find package for released package ${release.name}`,
);
}

return {
newVersion: release.newVersion,
oldVersion: release.oldVersion,
...packageJson,
};
});

for (const pkg of releasedPackages) {
for (const platform of config.platforms) {
await publishPlatform(pkg, platform);
}
}
Comment on lines +10 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The useChangesets config option is loaded but never used.

The configSchema defines a useChangesets option (default: true), but run() unconditionally calls getChangesetReleases() on line 16 regardless of this setting. If useChangesets is meant to be a toggle, the release retrieval should be conditional.

Proposed fix
 export async function run() {
 	const args = await getArgs();
 	const config = await loadConfig(args);
 	const root = await findRoot(process.cwd());
 	const { packages } = await getPackages(root.rootDir);
-	const { releases } = await util.getChangesetReleases();
+
+	if (!config.useChangesets) {
+		// TODO: Implement alternative release detection when changesets disabled
+		throw new Error("Publishing without changesets is not yet supported");
+	}
+
+	const { releases } = await util.getChangesetReleases();
🤖 Prompt for AI Agents
In @core/multipublish/src/index.ts around lines 11 - 39, run() currently always
calls util.getChangesetReleases() and ignores the config.useChangesets flag;
make release retrieval conditional: after loading config (the config variable),
check config.useChangesets (or its equivalent) and only call
util.getChangesetReleases() when true, otherwise set releases to an empty array
(or compute releasedPackages from an empty releases list) so downstream logic
using releasedPackages and publishPlatform(pkg, platform) behaves correctly;
update references around getChangesetReleases(), releasedPackages, and the loop
over releasedPackages to handle the case when changesets are disabled.

Comment on lines +30 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling for platform publishing failures.

If publishPlatform throws for one platform, the loop exits and subsequent platforms are not attempted. Consider wrapping in try-catch to collect failures and report them at the end, or add a --fail-fast flag to control behavior.

Proposed fix with error collection
+	const errors: Array<{ pkg: string; platform: string; error: unknown }> = [];
+
 	for (const pkg of releasedPackages) {
 		if (args.versionJsr) {
 			await updateJsrConfigVersion(pkg);
 			continue;
 		}
 
 		for (const platform of config.platforms) {
-			await publishPlatform(pkg, platform);
+			try {
+				await publishPlatform(pkg, platform);
+			} catch (error) {
+				const platformKey = typeof platform === "string" ? platform : platform[0];
+				errors.push({ pkg: pkg.packageJson.name, platform: platformKey, error });
+			}
 		}
 	}
+
+	if (errors.length > 0) {
+		for (const { pkg, platform, error } of errors) {
+			console.error(`Failed to publish ${pkg} to ${platform}:`, error);
+		}
+		throw new Error(`Publishing failed for ${errors.length} package/platform combination(s)`);
+	}
 }
🤖 Prompt for AI Agents
In @core/multipublish/src/index.ts around lines 32 - 41, The loop over
config.platforms currently calls publishPlatform(pkg, platform) without catching
errors so a single failure aborts remaining platforms; wrap the publishPlatform
call in a try/catch inside the inner loop (preserving the early continue for
args.versionJsr) and collect failures (e.g., push objects with pkg and platform
and error) into an array; if args.failFast is true rethrow immediately in the
catch, otherwise continue to next platform; after processing all
releasedPackages, if failures.length > 0 aggregate and report/log the failures
and exit non‑zero or throw a summarized Error so callers know which
pkg/platforms failed.

}
48 changes: 48 additions & 0 deletions core/multipublish/src/jsr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as z from "zod";

export const exportSchema = z.string().or(
z.record(
z.string(),
z.string().or(
z.object({
import: z.object({ default: z.string() }),
require: z.object({ default: z.string() }),
}),
),
),
);

export const packageJsonSchema = z.object({
exports: exportSchema,
license: z.string().optional(),
name: z.string().min(1),
version: z.string(),
});
Comment on lines +22 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding version format validation.

The version field accepts any string. Consider adding basic semver validation to catch malformed versions early, or at minimum a non-empty check like name.

♻️ Suggested improvement
 export const packageJsonSchema = z.object({
 	exports: exportSchema,
 	license: z.string().optional(),
 	name: z.string().min(1),
-	version: z.string(),
+	version: z.string().min(1),
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const packageJsonSchema = z.object({
exports: exportSchema,
license: z.string().optional(),
name: z.string().min(1),
version: z.string(),
});
export const packageJsonSchema = z.object({
exports: exportSchema,
license: z.string().optional(),
name: z.string().min(1),
version: z.string().min(1),
});
🤖 Prompt for AI Agents
In @core/multipublish/src/jsr.ts around lines 15 - 20, The packageJsonSchema
currently allows any string for the version field; change its validator in
packageJsonSchema so version is at least non-empty (e.g., use z.string().min(1))
or apply a basic semver regex check to validate format (e.g., match
/^\\d+\\.\\d+\\.\\d+(-[\\w.-]+)?(\\+[\\w.-]+)?$/) so malformed versions are
rejected early; update the version schema in packageJsonSchema accordingly and
ensure tests are updated to reflect the stricter validation.


export const jsrTransformer = packageJsonSchema.transform((schema) => ({
exports: convertPkgJsonExportsToJsr(schema.exports),
license: schema.license,
name: schema.name,
version: schema.version,
}));

export type JsrSchema = z.infer<typeof jsrSchema>;
export const jsrSchema = z.object({
exports: z
.string()
.or(z.array(z.string()))
.or(z.record(z.string(), z.string())),
license: z.string().optional(),
name: z.string().min(1),
version: z.string(),
});
Comment on lines +38 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Type inference order is unconventional but functional.

The JsrSchema type alias (line 29) references jsrSchema (line 30) before its declaration. This works due to TypeScript's hoisting of const declarations in type positions, but placing the type after the schema definition would improve readability and follow conventional patterns.

♻️ Suggested reorder
-export type JsrSchema = z.infer<typeof jsrSchema>;
 export const jsrSchema = z.object({
 	exclude: z.array(z.string()).optional(),
 	exports: z
 		.string()
 		.or(z.array(z.string()))
 		.or(z.record(z.string(), z.string())),
 	include: z.array(z.string()).optional(),
 	license: z.string().optional(),
 	name: z.string().min(1),
 	version: z.string(),
 });
+export type JsrSchema = z.infer<typeof jsrSchema>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type JsrSchema = z.infer<typeof jsrSchema>;
export const jsrSchema = z.object({
exclude: z.array(z.string()).optional(),
exports: z
.string()
.or(z.array(z.string()))
.or(z.record(z.string(), z.string())),
include: z.array(z.string()).optional(),
license: z.string().optional(),
name: z.string().min(1),
version: z.string(),
});
export const jsrSchema = z.object({
exclude: z.array(z.string()).optional(),
exports: z
.string()
.or(z.array(z.string()))
.or(z.record(z.string(), z.string())),
include: z.array(z.string()).optional(),
license: z.string().optional(),
name: z.string().min(1),
version: z.string(),
});
export type JsrSchema = z.infer<typeof jsrSchema>;
🤖 Prompt for AI Agents
In @core/multipublish/src/jsr.ts around lines 29 - 40, The JsrSchema type alias
references jsrSchema before it is declared which is legal but unconventional;
reorder by declaring jsrSchema first (export const jsrSchema = ...) and then
define export type JsrSchema = z.infer<typeof jsrSchema>; so the schema appears
before the derived type for clearer, more idiomatic code and readability.


function convertPkgJsonExportsToJsr(exports: z.infer<typeof exportSchema>) {
if (typeof exports === "string") return exports;
return Object.fromEntries(
Object.entries(exports).map(([key, value]) => [
key,
typeof value === "string" ? value : value.import.default,
]),
);
Comment on lines +99 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential runtime error if export object structure varies.

Line 104 accesses value.import.default but the exportSchema allows objects with both import and require keys. If an export object has a different structure (e.g., conditional exports with other keys), this will throw. Consider adding defensive checks or narrowing the schema.

Defensive check
-			typeof value === "string" ? value : value.import.default,
+			typeof value === "string"
+				? value
+				: value.import?.default ?? value.require?.default,
🤖 Prompt for AI Agents
In @core/multipublish/src/jsr.ts around lines 99 - 106, The
convertPkgJsonExportsToJsr function assumes each export value has an
import.default field and will throw for other conditional shapes; update the
mapping to defensively handle non-string/variant objects by checking for
value.import and value.import.default, then fallback to value.require,
value["default"], or other relevant keys (or a sensible default) before
returning—i.e., in the Object.entries map for convertPkgJsonExportsToJsr, add
type checks for value and optional chaining (or existence checks) to pick the
correct path value rather than directly accessing value.import.default.

}
Loading
Loading