diff --git a/.changeset/stupid-cycles-enjoy.md b/.changeset/stupid-cycles-enjoy.md
new file mode 100644
index 00000000..3f0f277e
--- /dev/null
+++ b/.changeset/stupid-cycles-enjoy.md
@@ -0,0 +1,13 @@
+---
+"@stephansama/multipublish": major
+---
+
+Initial release of `@stephansama/multipublish`, a CLI tool for publishing packages to multiple registries.
+
+Features:
+
+- **npm Publishing Strategies**: Supports two strategies for publishing to npm registries:
+ - `.npmrc` strategy: Temporarily creates or updates a root-level `.npmrc` file with registry and auth token details.
+ - `package.json` strategy: Directly sets the `publishConfig.registry` field in the package's `package.json`.
+- **JSR Support**: Automatically updates the `version` field in `jsr.json` before publishing.
+- **Configuration**: fully configurable via a schema using zod.
diff --git a/.config/.cspell.json b/.config/.cspell.json
index 99bd18af..cda58a5f 100644
--- a/.config/.cspell.json
+++ b/.config/.cspell.json
@@ -25,6 +25,7 @@
"kaomojis",
"macchiato",
"manypkg",
+ "multipublish",
"nodemon",
"nvim",
"nvmrc",
diff --git a/.config/.multipublishrc.json b/.config/.multipublishrc.json
new file mode 100644
index 00000000..bb142899
--- /dev/null
+++ b/.config/.multipublishrc.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../node_modules/@stephansama/multipublish/config/schema.json",
+ "platforms": [
+ [
+ "jsr",
+ {
+ "experimentalUpdateCatalogs": true,
+ "experimentalGenerateJSR": true
+ }
+ ],
+ [
+ "npm",
+ {
+ "registry": "npm.pkg.github.com",
+ "tokenEnvironmentKey": "GITHUB_TOKEN"
+ }
+ ]
+ ]
+}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3595fb9a..6902affd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,6 +16,7 @@ env:
TURBO_TEAM: ${{vars.TURBO_TEAM}}
permissions:
contents: write
+ packages: write
id-token: write
issues: write
pull-requests: write
@@ -67,8 +68,13 @@ jobs:
- if: github.ref_name == 'main'
name: 🦋 Create Changeset Release Pull Request
uses: changesets/action@v1
+ id: changesets
with:
commit: "chore: Update version for release"
title: "chore: Update version for release"
publish: pnpm run publish
createGithubReleases: true
+ - name: publish to other registries
+ if: steps.changesets.outputs.published == 'true'
+ run: |
+ echo "${{ steps.changesets.outputs.publishedPackages }}" | multipublish
diff --git a/.gitignore b/.gitignore
index c8539813..9c74c486 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/**/*
diff --git a/README.md b/README.md
index 2239ed71..782e477f 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ All packages are packaged underneath the `@stephansama` scope (for example: `@st
| [create-stephansama-example](core/example/README.md) |  |  | Download an example from the @stephansama/packages examples |
| [find-makefile-targets](core/find-makefile-targets/README.md) |  |  | Find makefile targets used to pipe into fzf |
| [github-env](core/github-env/README.md) |  |  | Additional environment variable types for GitHub CI |
+| [multipublish](core/multipublish/README.md) |  |  | Publish packages to multiple providers easily |
| [prettier-plugin-handlebars](core/prettier-plugin-handlebars/README.md) |  |  | Prettier plugin that automatically assigns the default parser for various handlebars files |
| [remark-asciinema](core/remark-asciinema/README.md) |  |  | A remark plugin that transforms Asciinema links into embedded players or screenshots. |
| [svelte-social-share-links](core/svelte-social-share-links/README.md) |  |  | Svelte/Web component to share the current url with various social media providers |
diff --git a/core/multipublish/README.md b/core/multipublish/README.md
new file mode 100644
index 00000000..40ba0812
--- /dev/null
+++ b/core/multipublish/README.md
@@ -0,0 +1,173 @@
+# @stephansama/multipublish
+
+[](https://github.com/stephansama/packages/tree/main/core/multipublish)
+[](https://packages.stephansama.info/api/@stephansama/multipublish)
+[](https://www.npmjs.com/package/@stephansama/multipublish)
+[](https://www.npmjs.com/package/@stephansama/multipublish)
+
+Publish packages to multiple providers easily
+
+##### Table of contents
+
+Open Table of contents
+
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Configuration](#configuration)
+ - [GitHub NPM Registry](#github-npm-registry)
+ - [JSR Configuration](#jsr-configuration)
+ - [Changesets](#changesets)
+ - [JSR](#jsr)
+ - [Published packages](#published-packages)
+
+
+
+## Installation
+
+```sh
+pnpm install @stephansama/multipublish
+```
+
+## Usage
+
+### Configuration
+
+You can configure `multipublish` by creating a configuration file (or object) in the root of your project. The following file formats are supported:
+
+- `package.json`
+- `.multipublishrc.cjs`
+- `.multipublishrc.js`
+- `.multipublishrc.json`
+- `.multipublishrc.mjs`
+- `.multipublishrc.ts`
+- `.multipublishrc.yaml`
+- `.multipublishrc.yml`
+- `.multipublishrc`
+- `.config/.multipublishrc.json`
+- `.config/.multipublishrc.yaml`
+- `.config/.multipublishrc.yml`
+- `.config/.multipublishrc`
+- `.config/multipublishrc.cjs`
+- `.config/multipublishrc.js`
+- `.config/multipublishrc.json`
+- `.config/multipublishrc.mjs`
+- `.config/multipublishrc.ts`
+- `.config/multipublishrc.yaml`
+- `.config/multipublishrc.yml`
+- `.config/multipublishrc`
+- `multipublish.config.cjs`
+- `multipublish.config.js`
+- `multipublish.config.mjs`
+- `multipublish.config.ts`
+
+```json
+{
+ "$schema": "./node_modules/@stephansama/multipublish/config/schema.json",
+ "platforms": [
+ ["jsr", { "experimentalGenerateJSR": true, "defaultExclude": ["!dist"] }],
+ [
+ "npm",
+ {
+ "registry": "https://npm.pkg.github.com",
+ "tokenEnvironmentKey": "GITHUB_TOKEN"
+ }
+ ]
+ ]
+}
+```
+
+### GitHub NPM Registry
+
+If publishing to the GitHub NPM registry, you must add `packages` to permissions when using a GitHub token. And allow `write` and `read` permissions for workflows (located in repo settings > actions > general).
+
+```yaml
+permissions:
+ packages: write
+```
+
+### JSR Configuration
+
+When publishing to JSR, you must either have a valid `jsr.json` or `deno.json`, or allow `experimentalGenerateJSR` using the config option.
+
+### Changesets
+
+If you are using jsr this with changesets, please update your version script with a preversion script that calls the `multipublish` CLI in order to update your existing jsr configurations.
+
+#### JSR
+
+```json
+{
+ "scripts": {
+ "preversion": "multipublish --useChangesetStatus --versionJsr",
+ "version": "changeset version"
+ }
+}
+```
+
+#### Published packages
+
+you can run multipublish after a changeset publish like so
+
+```yaml
+- if: github.ref_name == 'main'
+ name: 🦋 Create Changeset Release
+ uses: changesets/action@v1
+ id: changesets
+ with:
+ commit: "chore: Update version for release"
+ title: "chore: Update version for release"
+ publish: pnpm run publish
+ createGithubReleases: true
+- name: publish to other registries
+ if: steps.changesets.outputs.published == 'true'
+ run: |
+ echo "${{ steps.changesets.outputs.publishedPackages }}" | multipublish
+```
+
+
+
+# Zod Schema
+
+## Config
+
+_Object containing the following properties:_
+
+| Property | Type | Default |
+| :------------------- | :---------------------- | :----------- |
+| **`platforms`** (\*) | [Platforms](#platforms) | |
+| `tmpDirectory` | `string` | `'.release'` |
+| `useChangesets` | `boolean` | `true` |
+
+_(\*) Required._
+
+## JsrPlatformOptions
+
+_Object containing the following properties:_
+
+| Property | Type | Default |
+| :--------------------------- | :-------------- | :------ |
+| `allowSlowTypes` | `boolean` | `true` |
+| `defaultExclude` | `Array` | |
+| `defaultInclude` | `Array` | |
+| `experimentalGenerateJSR` | `boolean` | `false` |
+| `experimentalUpdateCatalogs` | `boolean` | `false` |
+
+_All properties are optional._
+
+## NpmPlatformOptions
+
+_Object containing the following properties:_
+
+| Property | Type | Default |
+| :-------------------- | :--------------------------- | :------------------------------ |
+| `registry` | `string` | `'https://registry.npmjs.org/'` |
+| `strategy` | `'.npmrc' \| 'package.json'` | `'.npmrc'` |
+| `tokenEnvironmentKey` | `string` | `'NODE_AUTH_TOKEN'` |
+
+_All properties are optional._
+
+## Platforms
+
+*Array of `'jsr' | 'npm'` *or\* _Tuple:_- `'jsr'`
- [JsrPlatformOptions](#jsrplatformoptions)
_or_ _Tuple:_- `'npm'`
- [NpmPlatformOptions](#npmplatformoptions)
items.\*
+
+
diff --git a/core/multipublish/build.mjs b/core/multipublish/build.mjs
new file mode 100644
index 00000000..2a0fa7bc
--- /dev/null
+++ b/core/multipublish/build.mjs
@@ -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,
+ });
+}
diff --git a/core/multipublish/cli.mjs b/core/multipublish/cli.mjs
new file mode 100755
index 00000000..f20d879d
--- /dev/null
+++ b/core/multipublish/cli.mjs
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+"use strict";
+
+import("./dist/index.js").then((mod) => mod.run());
diff --git a/core/multipublish/package.json b/core/multipublish/package.json
new file mode 100644
index 00000000..2a891f0a
--- /dev/null
+++ b/core/multipublish/package.json
@@ -0,0 +1,68 @@
+{
+ "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",
+ "bin": "./cli.mjs",
+ "files": [
+ "./dist",
+ "./config",
+ "./cli.mjs"
+ ],
+ "scripts": {
+ "build": "node build.mjs",
+ "dev": "node --watch build.mjs",
+ "lint": "eslint ./src/ --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",
+ "fast-glob": "^3.3.3",
+ "obug": "catalog:cli",
+ "package-manager-detector": "^1.6.0",
+ "yaml": "^2.8.2",
+ "yargs": "catalog:cli",
+ "zod": "catalog:schema"
+ },
+ "devDependencies": {
+ "@types/yargs": "catalog:",
+ "es-toolkit": "catalog:",
+ "jsr": "catalog:",
+ "tsdown": "catalog:"
+ },
+ "peerDependencies": {
+ "jsr": ">=0.10"
+ },
+ "packageManager": "pnpm@10.11.0",
+ "publishConfig": {
+ "access": "public",
+ "provenance": true
+ }
+}
diff --git a/core/multipublish/src/args.ts b/core/multipublish/src/args.ts
new file mode 100644
index 00000000..f8bbd9b4
--- /dev/null
+++ b/core/multipublish/src/args.ts
@@ -0,0 +1,59 @@
+import { enable } from "obug";
+import yargs, { type Options } from "yargs";
+import { hideBin } from "yargs/helpers";
+
+import { MODULE_NAME } from "./util";
+
+export type Args = Awaited>;
+
+let _args: Args | null = null;
+
+const args = {
+ config: { alias: "c", description: "Path to config file", type: "string" },
+ dry: { alias: "d", description: "Perform a dry run", type: "boolean" },
+ released: {
+ alias: "r",
+ description: "packages that have been updated and require a publish",
+ type: "array",
+ },
+ releasedFile: {
+ alias: "f",
+ description: "file denoting which packages have been updated",
+ type: "string",
+ },
+ useChangesetStatus: {
+ alias: "s",
+ description: "path to changeset status file used to version release",
+ type: "boolean",
+ },
+ verbose: {
+ alias: "v",
+ description: "Enable verbose logging",
+ type: "boolean",
+ },
+ versionJsr: {
+ alias: "j",
+ description: "update version jsr configuration files",
+ type: "boolean",
+ },
+} satisfies Record;
+
+export async function getArgs() {
+ return (_args ??= await parseArgs());
+}
+
+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;
+}
diff --git a/core/multipublish/src/catalog.test.ts b/core/multipublish/src/catalog.test.ts
new file mode 100644
index 00000000..29adbf49
--- /dev/null
+++ b/core/multipublish/src/catalog.test.ts
@@ -0,0 +1,144 @@
+import { merge } from "es-toolkit/compat";
+import * as path from "node:path";
+import { describe, expect, it, vi } from "vitest";
+
+import * as catalog from "./catalog";
+
+const mocks = vi.hoisted(() => ({
+ catalogLoadMap: { bun: vi.fn(), pnpm: vi.fn() },
+ writeFile: vi.fn(),
+}));
+
+vi.mock("./catalog", async (importOriginal) => {
+ const mod = await importOriginal();
+ return { ...mod, catalogLoadMap: mocks.catalogLoadMap };
+});
+
+vi.mock("node:fs/promises", async (importOriginal) => {
+ const mod = await importOriginal();
+ return { ...mod, writeFile: mocks.writeFile };
+});
+
+describe("catalog", () => {
+ describe("catalogSchema", () => {
+ it("should parse default catalog", () => {
+ const input = {
+ catalog: {
+ react: "^18.0.0",
+ },
+ };
+ const result = catalog.catalogSchema.parse(input);
+ expect(result.catalog).toEqual(input.catalog);
+ });
+
+ it("should parse named catalogs", () => {
+ const input = {
+ catalogs: {
+ react18: { react: "^18.0.0" },
+ react19: { react: "^19.0.0" },
+ },
+ };
+ const result = catalog.catalogSchema.parse(input);
+ expect(result.catalogs).toEqual(input.catalogs);
+ });
+ });
+
+ describe("loadVersion", () => {
+ const catalogs: catalog.CatalogSchema = {
+ catalog: {
+ foo: "1.0.0",
+ },
+ catalogs: {
+ legacy: {
+ foo: "0.9.0",
+ },
+ },
+ };
+
+ it("should return version if not using catalog", () => {
+ const result = catalog.loadVersion({
+ catalogs,
+ dependency: "foo",
+ version: "^2.0.0",
+ });
+ expect(result).toBe("^2.0.0");
+ });
+
+ it("should resolve default catalog", () => {
+ const result = catalog.loadVersion({
+ catalogs,
+ dependency: "foo",
+ version: "catalog:",
+ });
+ expect(result).toBe("1.0.0");
+ });
+
+ it("should resolve named catalog", () => {
+ const result = catalog.loadVersion({
+ catalogs,
+ dependency: "foo",
+ version: "catalog:legacy",
+ });
+ expect(result).toBe("0.9.0");
+ });
+
+ it("should throw if catalog dependency not found", () => {
+ expect(() => {
+ catalog.loadVersion({
+ catalogs,
+ dependency: "bar",
+ version: "catalog:",
+ });
+ }).toThrow();
+ });
+
+ it("should throw if named catalog not found", () => {
+ expect(() => {
+ catalog.loadVersion({
+ catalogs,
+ dependency: "foo",
+ version: "catalog:missing",
+ });
+ }).toThrow();
+ });
+ });
+
+ describe("updatePackageJsonWithCatalog", () => {
+ it("should update package.json for pnpm", async () => {
+ const mockDefaultCatalog = {
+ foo: "1.0.0",
+ };
+
+ const packageManager = "pnpm" as const;
+
+ mocks.catalogLoadMap[packageManager].mockResolvedValue({
+ catalog: mockDefaultCatalog,
+ });
+
+ const pkg = {
+ dir: "/path/to/pkg",
+ packageJson: {
+ dependencies: {
+ foo: "catalog:",
+ },
+ name: "example",
+ version: "0.0.0",
+ },
+ relativeDir: "./pkg",
+ };
+
+ const output = merge(pkg.packageJson, {
+ dependencies: {
+ foo: mockDefaultCatalog.foo,
+ },
+ });
+
+ await catalog.updatePackageJsonWithCatalog(pkg, packageManager);
+
+ expect(mocks.writeFile).toHaveBeenCalledWith(
+ path.join(pkg.dir, "package.json"),
+ JSON.stringify(output, undefined, 2),
+ );
+ });
+ });
+});
diff --git a/core/multipublish/src/catalog.ts b/core/multipublish/src/catalog.ts
new file mode 100644
index 00000000..64edead2
--- /dev/null
+++ b/core/multipublish/src/catalog.ts
@@ -0,0 +1,87 @@
+import { findRoot } from "@manypkg/find-root";
+import { getPackages, type Package } from "@manypkg/get-packages";
+import * as fsp from "node:fs/promises";
+import * as path from "node:path";
+import * as yaml from "yaml";
+import * as z from "zod";
+
+import type { AgentName } from "./detect";
+
+export type CatalogSchema = z.infer;
+export const catalogSchema = z.object({
+ catalog: z.record(z.string(), z.string()).optional(),
+ catalogs: z.record(z.string(), z.record(z.string(), z.string())).optional(),
+});
+
+export type ValidCatalogAgent = Extract;
+export const catalogLoadMap = {
+ bun: loadBunCatalogs,
+ pnpm: loadPnpmCatalogs,
+} satisfies Record Promise>;
+
+export async function loadBunCatalogs() {
+ const { rootDir } = await findRoot(process.cwd());
+ const { rootPackage } = await getPackages(rootDir);
+ const packageJson = rootPackage?.packageJson;
+ const catalogs = catalogSchema.parse(packageJson);
+ return catalogs;
+}
+
+export async function loadPnpmCatalogs() {
+ const { rootDir } = await findRoot(process.cwd());
+ const workspacePath = path.join(rootDir, "pnpm-workspace.yaml");
+ const workspaceFile = await fsp.readFile(workspacePath, {
+ encoding: "utf-8",
+ });
+ const catalogs = catalogSchema.parse(yaml.parse(workspaceFile));
+ return catalogs;
+}
+
+export function loadVersion({
+ catalogs,
+ dependency,
+ version,
+}: {
+ catalogs: CatalogSchema;
+ dependency: string;
+ version: string;
+}) {
+ if (!version.includes("catalog:")) return version;
+ const [_, catalogName] = version.split("catalog:");
+ const currentCatalog = catalogName
+ ? catalogs.catalogs?.[catalogName]
+ : catalogs.catalog;
+
+ const catalogVersion = currentCatalog?.[dependency];
+ if (!catalogVersion) {
+ throw new Error(
+ `no catalog version found for named catalog (${catalogName || "default"}) dependency (${dependency})`,
+ );
+ }
+
+ return catalogVersion;
+}
+
+export async function updatePackageJsonWithCatalog(
+ pkg: Package,
+ agent: ValidCatalogAgent,
+) {
+ const catalogStrategy = catalogLoadMap[agent];
+ const catalogs = await catalogStrategy();
+
+ for (const dependencyType of ["dependencies", "devDependencies"] as const) {
+ const entries = Object.entries(pkg.packageJson[dependencyType] || {});
+ for (const [dependency, version] of entries) {
+ pkg.packageJson[dependencyType] ??= {};
+ pkg.packageJson[dependencyType][dependency] = loadVersion({
+ catalogs,
+ dependency,
+ version,
+ });
+ }
+ }
+
+ const packageJsonPath = path.join(pkg.dir, "package.json");
+ const packageJsonFile = JSON.stringify(pkg.packageJson, undefined, 2);
+ await fsp.writeFile(packageJsonPath, packageJsonFile);
+}
diff --git a/core/multipublish/src/config.ts b/core/multipublish/src/config.ts
new file mode 100644
index 00000000..ad8c54bf
--- /dev/null
+++ b/core/multipublish/src/config.ts
@@ -0,0 +1,34 @@
+import { cosmiconfig, getDefaultSearchPlaces, type Options } from "cosmiconfig";
+
+import type { Args } from "./args";
+
+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(args: Args) {
+ const opts: Partial = { searchPlaces };
+
+ if (args.config) opts.searchPlaces = [args.config];
+
+ 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`,
+ ];
+}
diff --git a/core/multipublish/src/detect.ts b/core/multipublish/src/detect.ts
new file mode 100644
index 00000000..dfc79710
--- /dev/null
+++ b/core/multipublish/src/detect.ts
@@ -0,0 +1,16 @@
+import { detect } from "package-manager-detector/detect";
+
+export type AgentName = NonNullable>>["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;
+
+ return _detected;
+}
diff --git a/core/multipublish/src/index.test.ts b/core/multipublish/src/index.test.ts
new file mode 100644
index 00000000..268387d8
--- /dev/null
+++ b/core/multipublish/src/index.test.ts
@@ -0,0 +1,70 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { run } from "./index";
+
+const mocks = vi.hoisted(() => ({
+ findRoot: vi.fn(() => Promise.resolve({ rootDir: "/mock/root" })),
+ getArgs: vi.fn(() => Promise.resolve({ versionJsr: false })),
+ getPackages: vi.fn(() =>
+ Promise.resolve({
+ packages: [
+ {
+ dir: "/mock/root/pkg1",
+ packageJson: { name: "pkg1", version: "1.0.0" },
+ relativeDir: "pkg1",
+ },
+ ],
+ }),
+ ),
+ loadConfig: vi.fn(() => Promise.resolve({ platforms: ["npm"] })),
+ loadReleases: vi.fn(() =>
+ Promise.resolve([{ name: "pkg1", version: "1.0.0" }]),
+ ),
+ publishPlatform: vi.fn(),
+ updateJsrConfigVersion: vi.fn(),
+}));
+
+vi.mock("@manypkg/find-root", () => ({ findRoot: mocks.findRoot }));
+vi.mock("@manypkg/get-packages", () => ({ getPackages: mocks.getPackages }));
+
+vi.mock("./args", () => ({ getArgs: mocks.getArgs }));
+vi.mock("./config", () => ({ loadConfig: mocks.loadConfig }));
+vi.mock("./release", () => ({ loadReleases: mocks.loadReleases }));
+vi.mock("./publish", () => ({ publishPlatform: mocks.publishPlatform }));
+vi.mock("./jsr", () => ({
+ updateJsrConfigVersion: mocks.updateJsrConfigVersion,
+}));
+
+describe("run", () => {
+ afterEach(vi.clearAllMocks);
+
+ it("should call necessary functions and publish platforms", async () => {
+ await run();
+
+ expect(mocks.findRoot).toHaveBeenCalledOnce();
+ expect(mocks.getArgs).toHaveBeenCalledOnce();
+ expect(mocks.loadConfig).toHaveBeenCalledOnce();
+ expect(mocks.getPackages).toHaveBeenCalledOnce();
+ expect(mocks.loadReleases).toHaveBeenCalledOnce();
+ expect(mocks.updateJsrConfigVersion).not.toHaveBeenCalled();
+ expect(mocks.publishPlatform).toHaveBeenCalledOnce();
+ expect(mocks.publishPlatform).toHaveBeenCalledWith(
+ expect.objectContaining({
+ packageJson: expect.objectContaining({
+ name: "pkg1",
+ version: "1.0.0",
+ }),
+ }),
+ "npm",
+ );
+ });
+
+ it("should call updateJsrConfigVersion if args.versionJsr is true", async () => {
+ mocks.getArgs.mockResolvedValueOnce({ versionJsr: true });
+
+ await run();
+
+ expect(mocks.updateJsrConfigVersion).toHaveBeenCalledOnce();
+ expect(mocks.publishPlatform).not.toHaveBeenCalled();
+ });
+});
diff --git a/core/multipublish/src/index.ts b/core/multipublish/src/index.ts
new file mode 100644
index 00000000..793d6e4d
--- /dev/null
+++ b/core/multipublish/src/index.ts
@@ -0,0 +1,40 @@
+import { findRoot } from "@manypkg/find-root";
+import { getPackages } from "@manypkg/get-packages";
+
+import { getArgs } from "./args";
+import { loadConfig } from "./config";
+import { updateJsrConfigVersion } from "./jsr";
+import { publishPlatform } from "./publish";
+import { loadReleases } from "./release";
+
+export async function run() {
+ const root = await findRoot(process.cwd());
+ const args = await getArgs();
+ const config = await loadConfig(args);
+ const { packages } = await getPackages(root.rootDir);
+ const releases = await loadReleases(args);
+ const releasedPackages = releases.map((release) => {
+ const pkg = packages.find(
+ (curr) => curr.packageJson.name === release.name,
+ );
+
+ if (!pkg) {
+ throw new Error(
+ `unable to find package for released package ${release.name}`,
+ );
+ }
+
+ return { ...pkg, version: release.version || pkg.packageJson.version };
+ });
+
+ for (const pkg of releasedPackages) {
+ if (args.versionJsr) {
+ await updateJsrConfigVersion(pkg);
+ continue;
+ }
+
+ for (const platform of config.platforms) {
+ await publishPlatform(pkg, platform);
+ }
+ }
+}
diff --git a/core/multipublish/src/jsr.test.ts b/core/multipublish/src/jsr.test.ts
new file mode 100644
index 00000000..3f4383b3
--- /dev/null
+++ b/core/multipublish/src/jsr.test.ts
@@ -0,0 +1,89 @@
+import { describe, expect, it } from "vitest";
+
+import type { JsrSchema } from "./jsr";
+import type { JsrPlatformOptionsSchema } from "./schema";
+
+import { jsrTransformer, updateIncludeExcludeList } from "./jsr";
+
+describe("jsr", () => {
+ describe("jsrTransformer", () => {
+ it("should transform simple exports", () => {
+ const input = {
+ exports: "./index.js",
+ name: "@scope/pkg",
+ version: "1.0.0",
+ };
+ const result = jsrTransformer.parse(input);
+ expect(result.exports).toBe("./index.js");
+ });
+
+ it("should transform complex exports", () => {
+ const input = {
+ exports: {
+ ".": {
+ import: { default: "./index.js" },
+ require: { default: "./index.cjs" },
+ },
+ "./foo": "./foo.js",
+ },
+ name: "@scope/pkg",
+ version: "1.0.0",
+ };
+ const result = jsrTransformer.parse(input);
+ expect(result.exports).toEqual({
+ ".": "./index.js",
+ "./foo": "./foo.js",
+ });
+ });
+ });
+
+ describe("updateIncludeExcludeList", () => {
+ it("should add default include/exclude", () => {
+ const jsrConfig: JsrSchema = {
+ exports: "./index.ts",
+ name: "@scope/pkg",
+ version: "1.0.0",
+ };
+ const appConfig: JsrPlatformOptionsSchema = {
+ allowSlowTypes: true,
+ defaultExclude: ["test"],
+ defaultInclude: ["src"],
+ experimentalGenerateJSR: false,
+ experimentalUpdateCatalogs: false,
+ };
+
+ updateIncludeExcludeList(jsrConfig, appConfig);
+
+ expect(jsrConfig.include).toEqual(["src"]);
+ expect(jsrConfig.exclude).toEqual(["test"]);
+ });
+
+ it("should merge with existing include/exclude", () => {
+ const jsrConfig: JsrSchema = {
+ exclude: ["existing-exclude"],
+ exports: "./index.ts",
+ include: ["existing-include"],
+ name: "@scope/pkg",
+ version: "1.0.0",
+ };
+ const appConfig: JsrPlatformOptionsSchema = {
+ allowSlowTypes: true,
+ defaultExclude: ["new-exclude"],
+ defaultInclude: ["new-include"],
+ experimentalGenerateJSR: false,
+ experimentalUpdateCatalogs: false,
+ };
+
+ updateIncludeExcludeList(jsrConfig, appConfig);
+
+ expect(jsrConfig.include).toEqual([
+ "existing-include",
+ "new-include",
+ ]);
+ expect(jsrConfig.exclude).toEqual([
+ "existing-exclude",
+ "new-exclude",
+ ]);
+ });
+ });
+});
diff --git a/core/multipublish/src/jsr.ts b/core/multipublish/src/jsr.ts
new file mode 100644
index 00000000..d46d9147
--- /dev/null
+++ b/core/multipublish/src/jsr.ts
@@ -0,0 +1,111 @@
+import type { Package } from "@manypkg/get-packages";
+
+import fg from "fast-glob";
+import * as fsp from "node:fs/promises";
+import * as z from "zod";
+
+import type { JsrPlatformOptionsSchema } from "./schema";
+
+type ExportsSchema = z.infer;
+export const exportSchema = z.string().or(
+ z.record(
+ z.string(),
+ z.string().or(
+ z.object({
+ import: z.object({ default: z.string() }).or(z.string()),
+ require: z.object({ default: z.string() }).or(z.string()),
+ }),
+ ),
+ ),
+);
+
+export const packageJsonSchema = z.object({
+ exports: exportSchema,
+ license: z.string().optional(),
+ name: z.string().min(1),
+ version: z.string(),
+});
+
+export const jsrTransformer = packageJsonSchema.transform((schema) => ({
+ exports: convertPkgJsonExportsToJsr(schema.exports),
+ license: schema.license,
+ name: schema.name,
+ version: schema.version,
+}));
+
+export { jsrTransformer as transformer };
+
+export type JsrSchema = z.infer;
+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 async function loadConfig(basePath: string) {
+ const files = await fg(basePath + "/{deno,jsr}.json{,c}");
+ if (files.length > 1) {
+ throw new Error("please only have one deno or jsr configuration file");
+ }
+
+ const configFile = files.at(0);
+ if (!configFile) {
+ console.info("no jsr config file found");
+ return { config: null, filename: undefined };
+ }
+
+ const file = await fsp.readFile(configFile, { encoding: "utf8" });
+ return { config: jsrSchema.parse(JSON.parse(file)), filename: configFile };
+}
+
+export function updateIncludeExcludeList(
+ jsrConfig: JsrSchema,
+ appConfig: JsrPlatformOptionsSchema,
+) {
+ for (const key of ["include", "exclude"] as const) {
+ const capitalizedKey = key.at(0)?.toUpperCase() + key.slice(1);
+ const appConfigKey =
+ `default${capitalizedKey}` as `default${Capitalize}`;
+ const appConfigList = appConfig[appConfigKey] || [];
+
+ jsrConfig[key] ??= [];
+ jsrConfig[key] = [...jsrConfig[key], ...appConfigList];
+ }
+}
+
+export async function updateJsrConfigVersion(
+ pkg: Package & { version: string },
+) {
+ const userJsr = await loadConfig(pkg.dir);
+ if (!userJsr.config || !userJsr.filename) {
+ throw new Error("unable to load user provided deno/jsr config file");
+ }
+
+ userJsr.config.version = pkg.version;
+
+ await fsp.writeFile(
+ userJsr.filename,
+ JSON.stringify(userJsr.config, undefined, 2),
+ "utf8",
+ );
+}
+
+function convertPkgJsonExportsToJsr(exports: ExportsSchema) {
+ if (typeof exports === "string") return exports;
+ return Object.fromEntries(
+ Object.entries(exports).map(([key, value]) => [
+ key,
+ typeof value === "string"
+ ? value
+ : typeof value.import === "string"
+ ? value.import
+ : value.import.default,
+ ]),
+ );
+}
diff --git a/core/multipublish/src/publish.test.ts b/core/multipublish/src/publish.test.ts
new file mode 100644
index 00000000..7234129a
--- /dev/null
+++ b/core/multipublish/src/publish.test.ts
@@ -0,0 +1,187 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { publishPlatform } from "./publish";
+
+const mocks = vi.hoisted(() => ({
+ detectPackageManager: vi.fn(),
+ execFileSync: vi.fn(),
+ execSync: vi.fn(),
+ existsSync: vi.fn(),
+ findRoot: vi.fn(),
+ getArgs: vi.fn(),
+ loadConfig: vi.fn(),
+ readFile: vi.fn(),
+ writeFile: vi.fn(),
+}));
+
+vi.mock("@manypkg/find-root", () => ({
+ findRoot: mocks.findRoot,
+}));
+
+vi.mock("node:fs", async (importOriginal) => {
+ const mod = await importOriginal();
+ return {
+ ...mod,
+ existsSync: mocks.existsSync,
+ };
+});
+
+vi.mock("node:child_process", () => ({
+ execFileSync: mocks.execFileSync,
+ execSync: mocks.execSync,
+}));
+
+vi.mock("node:fs/promises", async (importOriginal) => {
+ const mod = await importOriginal();
+ return {
+ ...mod,
+ readFile: mocks.readFile,
+ writeFile: mocks.writeFile,
+ };
+});
+
+vi.mock("./args", () => ({ getArgs: mocks.getArgs }));
+
+vi.mock("./detect", () => ({
+ detectPackageManager: mocks.detectPackageManager,
+}));
+
+vi.mock("./util", async (importOriginal) => {
+ const mod = await importOriginal();
+ return {
+ ...mod,
+ chdir: vi.fn(async (_dir, cb) => {
+ await cb();
+ }),
+ gitClean: vi.fn(),
+ };
+});
+
+vi.mock("./jsr", async (importOriginal) => {
+ const mod = await importOriginal();
+ return {
+ ...mod,
+ loadConfig: mocks.loadConfig,
+ updateIncludeExcludeList: vi.fn(),
+ };
+});
+
+describe("publish", () => {
+ beforeEach(() => {
+ mocks.getArgs.mockResolvedValue({ dry: false });
+ mocks.detectPackageManager.mockResolvedValue("pnpm");
+ mocks.findRoot.mockResolvedValue({ rootDir: "/fake/root" });
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ vi.unstubAllEnvs();
+ });
+
+ describe("publishPlatform", () => {
+ it("should publish to npm with package.json strategy", async () => {
+ const pkg = {
+ dir: "/path/to/pkg",
+ packageJson: {
+ name: "test-pkg",
+ version: "1.0.0",
+ },
+ relativeDir: "./pkg",
+ };
+
+ await publishPlatform(pkg, [
+ "npm",
+ {
+ registry: "https://registry.npmjs.org/",
+ strategy: "package.json",
+ },
+ ]);
+
+ expect(mocks.writeFile).toHaveBeenCalledWith(
+ "/path/to/pkg/package.json",
+ JSON.stringify(
+ {
+ ...pkg.packageJson,
+ publishConfig: {
+ registry: "https://registry.npmjs.org/",
+ },
+ },
+ undefined,
+ 2,
+ ),
+ );
+
+ expect(mocks.execSync).toHaveBeenCalledWith(
+ "pnpm publish --no-git-checks",
+ { stdio: "inherit" },
+ );
+ });
+
+ it("should publish to npm with .npmrc strategy", async () => {
+ mocks.existsSync.mockReturnValue(false);
+ vi.stubEnv("NPM_TOKEN", "test-token");
+
+ const pkg = {
+ dir: "/path/to/pkg",
+ packageJson: {
+ name: "@scope/test-pkg",
+ version: "1.0.0",
+ },
+ relativeDir: "./pkg",
+ } as const;
+
+ await publishPlatform(pkg, [
+ "npm",
+ {
+ registry: "https://registry.npmjs.org/",
+ strategy: ".npmrc",
+ tokenEnvironmentKey: "NPM_TOKEN",
+ },
+ ]);
+
+ expect(mocks.writeFile).toHaveBeenCalledWith(
+ "/fake/root/.npmrc",
+ expect.stringContaining("test-token"),
+ );
+
+ expect(mocks.execSync).toHaveBeenCalledWith(
+ "pnpm publish --no-git-checks",
+ { stdio: "inherit" },
+ );
+ });
+
+ it("should publish to jsr", async () => {
+ vi.mocked(mocks.loadConfig).mockResolvedValue({
+ config: {
+ exports: "index.ts",
+ name: "@scope/pkg",
+ version: "1.0.0",
+ },
+ filename: "/path/to/pkg/jsr.json",
+ });
+
+ const pkg = {
+ dir: "/path/to/pkg",
+ packageJson: {
+ name: "@scope/pkg",
+ version: "1.0.0",
+ },
+ relativeDir: "./pkg",
+ };
+
+ const platform = "jsr";
+
+ await publishPlatform(pkg, platform);
+
+ expect(mocks.writeFile).toHaveBeenCalledWith(
+ "/path/to/pkg/jsr.json",
+ expect.any(String),
+ );
+
+ expect(mocks.execSync).toHaveBeenCalledWith(
+ "pnpm dlx jsr publish --allow-dirty --allow-slow-types",
+ { stdio: "inherit" },
+ );
+ });
+ });
+});
diff --git a/core/multipublish/src/publish.ts b/core/multipublish/src/publish.ts
new file mode 100644
index 00000000..c1473827
--- /dev/null
+++ b/core/multipublish/src/publish.ts
@@ -0,0 +1,168 @@
+import type { Package } from "@manypkg/get-packages";
+
+import { findRoot } from "@manypkg/find-root";
+import * as cp from "node:child_process";
+import * as fs from "node:fs";
+import * as fsp from "node:fs/promises";
+import * as path from "node:path";
+
+import type { Config } from "./schema";
+
+import { getArgs } from "./args";
+import { updatePackageJsonWithCatalog } from "./catalog";
+import { type AgentName, detectPackageManager } from "./detect";
+import * as jsr from "./jsr";
+import { jsrPlatformOptionsSchema, npmPlatformOptionsSchema } from "./schema";
+import * as util from "./util";
+
+export const npmPublishCommand = {
+ bun: "bun publish",
+ npm: "npm publish",
+ pnpm: "pnpm publish",
+ yarn: "yarn publish",
+} satisfies Record, string>;
+
+export const jsrPublishCommand = {
+ bun: "bunx publish",
+ deno: "deno publish",
+ npm: "npx jsr publish",
+ pnpm: "pnpm dlx jsr publish",
+ yarn: "yarn dlx jsr publish",
+} satisfies Record;
+
+export async function publishPlatform(
+ pkg: Package,
+ platform: Config["platforms"][number],
+) {
+ const packageManager = await detectPackageManager();
+ const isString = typeof platform === "string";
+ const key = isString ? platform : platform[0];
+ const rawConfig = isString ? {} : platform[1];
+ const args = await getArgs();
+ const isDryRun = !!args.dry;
+ const packageJsonPath = path.join(pkg.dir, "package.json");
+
+ switch (key) {
+ case "jsr": {
+ const config = jsrPlatformOptionsSchema.parse(rawConfig);
+ const userJsr = await jsr.loadConfig(pkg.dir);
+
+ if (config.experimentalGenerateJSR) {
+ userJsr.config = jsr.transformer.parse(pkg.packageJson);
+ userJsr.filename = path.join(pkg.dir, util.JSR_CONFIG_FILENAME);
+ }
+
+ if (!userJsr.config) {
+ throw new Error("failed to load userJsr config file");
+ }
+
+ if (!userJsr.filename) {
+ throw new Error("failed to load userJsr config filename");
+ }
+
+ jsr.updateIncludeExcludeList(userJsr.config, config);
+
+ const jsrFile = JSON.stringify(userJsr.config, undefined, 2);
+ await fsp.writeFile(userJsr.filename, jsrFile);
+
+ if (config.experimentalUpdateCatalogs) {
+ if (packageManager === "pnpm" || packageManager === "bun") {
+ await updatePackageJsonWithCatalog(pkg, packageManager);
+ } else {
+ console.error(
+ `attempted to update catalogs with an unsupported package manager ${packageManager}`,
+ );
+ }
+ }
+
+ await util.chdir(pkg.dir, () => {
+ cp.execSync(
+ [
+ jsrPublishCommand[packageManager],
+ "--allow-dirty",
+ isDryRun && "--dry-run",
+ config.allowSlowTypes && "--allow-slow-types",
+ ]
+ .filter((x) => x)
+ .join(" "),
+ { stdio: "inherit" },
+ );
+ });
+
+ util.gitClean(userJsr.filename);
+ if (config.experimentalUpdateCatalogs) {
+ util.gitClean(packageJsonPath);
+ }
+
+ break;
+ }
+ case "npm": {
+ const { rootDir } = await findRoot(process.cwd());
+ const npmrcPath = path.join(rootDir, ".npmrc");
+ const config = npmPlatformOptionsSchema.parse(rawConfig);
+
+ if (packageManager === "deno") {
+ throw new Error("deno is not supported for npm publish");
+ }
+
+ switch (config.strategy) {
+ case ".npmrc": {
+ const authToken = process.env[config.tokenEnvironmentKey];
+ if (!authToken) {
+ throw new Error(
+ "no auth token provided. please use an auth token with npmrc strategy",
+ );
+ }
+
+ const npmrcPrefix = fs.existsSync(npmrcPath)
+ ? await fsp.readFile(npmrcPath, "utf8")
+ : "";
+
+ const scope = pkg.packageJson.name.split("/").at(0);
+ if (!scope?.startsWith("@")) {
+ throw new Error("scope must start with `@` symbol");
+ }
+
+ const npmrcFile =
+ npmrcPrefix +
+ "\n" +
+ util.npmrcTemplate({
+ authToken,
+ registry: config.registry,
+ registryDomain: new URL(config.registry).host,
+ scope,
+ });
+
+ await fsp.writeFile(npmrcPath, npmrcFile);
+ break;
+ }
+ case "package.json": {
+ pkg.packageJson.publishConfig ??= {};
+ pkg.packageJson.publishConfig.registry = config.registry;
+ const file = JSON.stringify(pkg.packageJson, undefined, 2);
+ await fsp.writeFile(packageJsonPath, file);
+ break;
+ }
+ }
+
+ await util.chdir(pkg.dir, () => {
+ cp.execSync(
+ [
+ npmPublishCommand[packageManager],
+ packageManager === "pnpm" && "--no-git-checks",
+ isDryRun && "--dry-run",
+ ]
+ .filter((x) => x)
+ .join(" "),
+ { stdio: "inherit" },
+ );
+ });
+
+ util.gitClean(packageJsonPath);
+ if (config.strategy === ".npmrc") util.gitClean(npmrcPath);
+ break;
+ }
+ default:
+ throw new Error(`no implementation found for ${key}`);
+ }
+}
diff --git a/core/multipublish/src/release.test.ts b/core/multipublish/src/release.test.ts
new file mode 100644
index 00000000..9dd2b3bb
--- /dev/null
+++ b/core/multipublish/src/release.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+
+import { changesetStatusSchema } from "./release";
+
+describe("release", () => {
+ describe("changesetStatusSchema", () => {
+ it("should transform changeset status to releases", () => {
+ const input = {
+ changesets: [],
+ releases: [
+ {
+ changesets: [],
+ name: "pkg-a",
+ newVersion: "1.0.1",
+ oldVersion: "1.0.0",
+ type: "patch",
+ },
+ {
+ changesets: [],
+ name: "pkg-b",
+ newVersion: "2.1.0",
+ oldVersion: "2.0.0",
+ type: "minor",
+ },
+ ],
+ };
+
+ const expected = [
+ { name: "pkg-a", version: "1.0.1" },
+ { name: "pkg-b", version: "2.1.0" },
+ ];
+
+ const result = changesetStatusSchema.parse(input);
+ expect(result).toEqual(expected);
+ });
+ });
+});
diff --git a/core/multipublish/src/release.ts b/core/multipublish/src/release.ts
new file mode 100644
index 00000000..b55cc03c
--- /dev/null
+++ b/core/multipublish/src/release.ts
@@ -0,0 +1,56 @@
+import * as cp from "node:child_process";
+import * as fsp from "node:fs/promises";
+import * as z from "zod";
+
+import type { Args } from "./args";
+
+import { gitClean, readStdin } from "./util";
+
+export type ReleaseSchema = z.infer;
+export const releaseSchema = z.object({
+ name: z.string(),
+ version: z.string().optional(),
+});
+
+export type ReleasesSchema = z.infer;
+export const releasesSchema = z.array(releaseSchema);
+
+export type ChangesetStatusSchema = z.input;
+export const changesetStatusSchema = z
+ .object({
+ releases: z.array(
+ z.object({ name: z.string(), newVersion: z.string() }),
+ ),
+ })
+ .transform((schema) =>
+ schema.releases.map((release) => ({
+ name: release.name,
+ version: release.newVersion,
+ })),
+ );
+
+export async function loadReleases(args: Args) {
+ if (args.released) {
+ return releasesSchema.parse(args.released.map((name) => ({ name })));
+ }
+
+ if (args.releasedFile) {
+ const releasedFile = await fsp.readFile(args.releasedFile, "utf8");
+ const releasedInfo = JSON.parse(releasedFile);
+ return releasesSchema.parse(releasedInfo);
+ }
+
+ if (args.useChangesetStatus) {
+ const changesetOutput = ".multipublish.status.json";
+ cp.execFileSync("changeset", ["status", `--output=${changesetOutput}`]);
+
+ const file = await fsp.readFile(changesetOutput, "utf8");
+
+ gitClean(changesetOutput);
+
+ return changesetStatusSchema.parse(JSON.parse(file));
+ }
+
+ const input = await readStdin();
+ return releasesSchema.parse(input);
+}
diff --git a/core/multipublish/src/schema.test.ts b/core/multipublish/src/schema.test.ts
new file mode 100644
index 00000000..6beb0a7f
--- /dev/null
+++ b/core/multipublish/src/schema.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from "vitest";
+
+import { configSchema, platformsSchema } from "./schema";
+
+describe("schema", () => {
+ describe("platformsSchema", () => {
+ it("should accept simple platform strings", () => {
+ const valid = ["npm", "jsr"];
+ const result = platformsSchema.parse(valid);
+ expect(result).toEqual(valid);
+ });
+
+ it("should accept platform tuples with options", () => {
+ const input = [
+ ["npm", { registry: "https://custom.registry" }],
+ ["jsr", { allowSlowTypes: false }],
+ ];
+ const expected = [
+ [
+ "npm",
+ {
+ registry: "https://custom.registry",
+ strategy: ".npmrc",
+ tokenEnvironmentKey: "NODE_AUTH_TOKEN",
+ },
+ ],
+ [
+ "jsr",
+ {
+ allowSlowTypes: false,
+ experimentalGenerateJSR: false,
+ experimentalUpdateCatalogs: false,
+ },
+ ],
+ ];
+ const result = platformsSchema.parse(input);
+ expect(result).toEqual(expected);
+ });
+
+ it("should reject invalid platforms", () => {
+ expect(() => {
+ platformsSchema.parse(["invalid"]);
+ }).toThrow();
+ });
+ });
+
+ describe("configSchema", () => {
+ it("should accept valid config", () => {
+ const config = {
+ platforms: ["npm"],
+ tmpDirectory: "custom-tmp",
+ useChangesets: false,
+ };
+ const result = configSchema.parse(config);
+ expect(result).toEqual(config);
+ });
+
+ it("should use default values", () => {
+ const config = {
+ platforms: ["jsr"],
+ };
+ const result = configSchema.parse(config);
+ expect(result.tmpDirectory).toBe(".release");
+ expect(result.useChangesets).toBe(true);
+ });
+
+ it("should reject missing required fields", () => {
+ expect(() => {
+ configSchema.parse({}); // platforms is required
+ }).toThrow();
+ });
+ });
+});
diff --git a/core/multipublish/src/schema.ts b/core/multipublish/src/schema.ts
new file mode 100644
index 00000000..b8d75ba2
--- /dev/null
+++ b/core/multipublish/src/schema.ts
@@ -0,0 +1,33 @@
+import * as z from "zod";
+
+export type JsrPlatformOptionsSchema = z.infer;
+export const jsrPlatformOptionsSchema = z.object({
+ allowSlowTypes: z.boolean().default(true),
+ defaultExclude: z.array(z.string()).optional(),
+ defaultInclude: z.array(z.string()).optional(),
+ experimentalGenerateJSR: z.boolean().default(false),
+ experimentalUpdateCatalogs: z.boolean().default(false),
+});
+
+export type NpmPlatformOptionsSchema = z.infer;
+export const npmPlatformOptionsSchema = z.object({
+ registry: z.string().default("https://registry.npmjs.org/"),
+ strategy: z.enum([".npmrc", "package.json"]).default(".npmrc"),
+ tokenEnvironmentKey: z.string().default("NODE_AUTH_TOKEN"),
+});
+
+export type PlatformsSchema = z.input;
+export const platformsSchema = z.array(
+ z
+ .literal("jsr")
+ .or(z.literal("npm"))
+ .or(z.tuple([z.literal("jsr"), jsrPlatformOptionsSchema]))
+ .or(z.tuple([z.literal("npm"), npmPlatformOptionsSchema])),
+);
+
+export type Config = z.input;
+export const configSchema = z.object({
+ platforms: platformsSchema,
+ tmpDirectory: z.string().default(".release"),
+ useChangesets: z.boolean().default(true),
+});
diff --git a/core/multipublish/src/util.test.ts b/core/multipublish/src/util.test.ts
new file mode 100644
index 00000000..7d22aebe
--- /dev/null
+++ b/core/multipublish/src/util.test.ts
@@ -0,0 +1,92 @@
+import { merge } from "es-toolkit/compat";
+import * as fs from "node:fs";
+import * as os from "node:os";
+import * as process from "node:process";
+import { describe, expect, it, vi } from "vitest";
+
+import { chdir, gitClean, npmrcTemplate, readStdin } from "./util";
+
+const mocks = vi.hoisted(() => ({
+ asyncIterator: vi.fn(),
+ on: vi.fn(),
+ setEncoding: vi.fn(),
+}));
+
+vi.mock("node:process", async (importOriginal) => {
+ const originalProcess = await importOriginal();
+ return merge(originalProcess, {
+ stdin: {
+ on: mocks.on,
+ setEncoding: mocks.setEncoding,
+ [Symbol.asyncIterator]: mocks.asyncIterator,
+ },
+ });
+});
+
+describe("chdir", () => {
+ it("should change directory, execute callback, and then change back", async () => {
+ const initialDir = fs.realpathSync(process.cwd());
+ const tempDir = fs.realpathSync(os.tmpdir());
+
+ let callbackDir: string | undefined;
+
+ await chdir(tempDir, () => {
+ callbackDir = fs.realpathSync(process.cwd());
+ });
+
+ const finalDir = fs.realpathSync(process.cwd());
+
+ expect(callbackDir).toBe(tempDir);
+ expect(finalDir).toBe(initialDir);
+ });
+});
+
+describe("gitClean", () => {
+ it("should remove the specified file", () => {
+ const tempFile = "dummy-file-for-testing.tmp";
+ fs.writeFileSync(tempFile, "delete me");
+
+ gitClean(tempFile);
+
+ expect(fs.existsSync(tempFile)).toBe(false);
+ });
+});
+
+describe("npmrcTemplate", () => {
+ it("should generate correct .npmrc content", () => {
+ const opts = {
+ authToken: "my-secret-token",
+ registry: "https://registry.npmjs.org/",
+ registryDomain: "registry.npmjs.org",
+ scope: "@my-scope",
+ };
+
+ const expected = `
+@my-scope:registry=https://registry.npmjs.org/
+//registry.npmjs.org/:_authToken=my-secret-token
+`.trim();
+
+ const result = npmrcTemplate(opts);
+ expect(result.trim()).toBe(expected);
+ });
+});
+
+describe("readStdin", () => {
+ it("should return null if stdin is a TTY", async () => {
+ vi.mocked(process.stdin).isTTY = true;
+ const result = await readStdin();
+ expect(result).toBeNull();
+ });
+
+ it("should read from stdin", async () => {
+ vi.mocked(process.stdin).isTTY = false;
+ const input = "hello world";
+
+ mocks.asyncIterator.mockImplementation(async function* () {
+ yield input;
+ });
+
+ const result = await readStdin();
+ expect(result).toBe(input);
+ });
+});
diff --git a/core/multipublish/src/util.ts b/core/multipublish/src/util.ts
new file mode 100644
index 00000000..9430fdaa
--- /dev/null
+++ b/core/multipublish/src/util.ts
@@ -0,0 +1,51 @@
+import dedent from "dedent";
+import * as cp from "node:child_process";
+import * as process from "node:process";
+
+export const MODULE_NAME = "multipublish" as const;
+export const JSR_CONFIG_FILENAME = "jsr.json" as const;
+
+export async function chdir(
+ newDir: string,
+ callback: () => Promise | void,
+) {
+ const cwd = process.cwd();
+ try {
+ process.chdir(newDir);
+ await callback();
+ } catch (e) {
+ console.error(e);
+ } finally {
+ process.chdir(cwd);
+ }
+}
+
+export function gitClean(filename: string) {
+ cp.execFileSync("git", ["clean", "-f", "--", filename], {
+ stdio: "inherit",
+ });
+}
+
+export function npmrcTemplate(opts: {
+ authToken: string;
+ registry: string;
+ registryDomain: string;
+ scope: string;
+}) {
+ return dedent`
+ {{SCOPE}}:registry={{REGISTRY}}
+ //{{REGISTRY_DOMAIN}}/:_authToken={{AUTH_TOKEN}}
+ `
+ .replace("{{AUTH_TOKEN}}", opts.authToken)
+ .replace("{{REGISTRY}}", opts.registry)
+ .replace("{{REGISTRY_DOMAIN}}", opts.registryDomain)
+ .replace("{{SCOPE}}", opts.scope);
+}
+
+export async function readStdin() {
+ process.stdin.setEncoding("utf8");
+ if (process.stdin.isTTY) return null;
+ let chunks = "";
+ for await (const chunk of process.stdin) chunks += chunk;
+ return chunks;
+}
diff --git a/core/multipublish/tsconfig.json b/core/multipublish/tsconfig.json
new file mode 100644
index 00000000..bd3334a8
--- /dev/null
+++ b/core/multipublish/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": ["../../tsconfig.base.json"],
+ "include": ["./src/**/*"]
+}
diff --git a/core/multipublish/typedoc.json b/core/multipublish/typedoc.json
new file mode 100644
index 00000000..57eeff2c
--- /dev/null
+++ b/core/multipublish/typedoc.json
@@ -0,0 +1,12 @@
+{
+ "entryPoints": ["src/*"],
+ "tsconfig": "./tsconfig.json",
+ "exclude": [
+ "**/tests/**",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "node_modules",
+ "**/{node_modules,test,book,doc,dist}/**/*",
+ "**/{pages,components}/**"
+ ]
+}
diff --git a/core/multipublish/vitest.config.ts b/core/multipublish/vitest.config.ts
new file mode 100644
index 00000000..2fb5c48d
--- /dev/null
+++ b/core/multipublish/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ },
+});
diff --git a/package.json b/package.json
index 57cfbd03..d0c56ae4 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,7 @@
"pretest:ci": "pnpm run scripts:generate-examples",
"test:ci": "vitest --run --coverage --reporter=junit --outputFile=test-report.junit.xml",
"test:ui": "vitest --ui",
- "version": "changeset version && pnpm install --lockfile-only",
- "with-env": "dotenv -e .env --"
+ "version": "changeset version && pnpm install --lockfile-only"
},
"dependencies": {
"@changesets/cli": "^2.29.8",
@@ -44,9 +43,10 @@
"@commitlint/types": "^19.8.1",
"@eslint/js": "^9.39.1",
"@eslint/json": "^0.14.0",
- "@manypkg/get-packages": "^3.1.0",
+ "@manypkg/get-packages": "catalog:",
"@stephansama/ai-commit-msg": "workspace:*",
"@stephansama/auto-readme": "workspace:*",
+ "@stephansama/multipublish": "workspace:*",
"@stephansama/prettier-plugin-handlebars": "workspace:*",
"@tsconfig/recommended": "^1.0.13",
"@turbo/gen": "^2.7.0",
@@ -54,7 +54,6 @@
"@typescript-eslint/parser": "^8.48.1",
"@vitest/coverage-v8": "catalog:vitest",
"@vitest/ui": "catalog:vitest",
- "dotenv-cli": "^8.0.0",
"eslint": "^9.39.1",
"eslint-config-flat-gitignore": "^2.1.0",
"eslint-config-prettier": "^10.1.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7fd9d95e..aaafba2b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,6 +25,12 @@ catalogs:
'@iconify/utils':
specifier: ^2.3.0
version: 2.3.0
+ '@manypkg/find-root':
+ specifier: ^3.1.0
+ version: 3.1.0
+ '@manypkg/get-packages':
+ specifier: ^3.1.0
+ version: 3.1.0
'@testing-library/jest-dom':
specifier: ^6.9.1
version: 6.9.1
@@ -64,6 +70,9 @@ catalogs:
happy-dom:
specifier: ^20.0.11
version: 20.0.11
+ jsr:
+ specifier: ^0.13.5
+ version: 0.13.5
mdast:
specifier: ^3.0.0
version: 3.0.0
@@ -190,7 +199,7 @@ importers:
specifier: ^0.14.0
version: 0.14.0
'@manypkg/get-packages':
- specifier: ^3.1.0
+ specifier: 'catalog:'
version: 3.1.0
'@stephansama/ai-commit-msg':
specifier: workspace:*
@@ -198,6 +207,9 @@ importers:
'@stephansama/auto-readme':
specifier: workspace:*
version: link:core/auto-readme
+ '@stephansama/multipublish':
+ specifier: workspace:*
+ version: link:core/multipublish
'@stephansama/prettier-plugin-handlebars':
specifier: workspace:*
version: link:core/prettier-plugin-handlebars
@@ -219,9 +231,6 @@ importers:
'@vitest/ui':
specifier: catalog:vitest
version: 4.0.15(vitest@4.0.15)
- dotenv-cli:
- specifier: ^8.0.0
- version: 8.0.0
eslint:
specifier: ^9.39.1
version: 9.39.1(jiti@2.6.1)
@@ -572,6 +581,52 @@ importers:
core/github-env: {}
+ core/multipublish:
+ dependencies:
+ '@manypkg/find-root':
+ specifier: 'catalog:'
+ version: 3.1.0
+ '@manypkg/get-packages':
+ specifier: 'catalog:'
+ version: 3.1.0
+ cosmiconfig:
+ specifier: catalog:cli
+ version: 9.0.0(typescript@5.9.3)
+ dedent:
+ specifier: ^1.7.1
+ version: 1.7.1
+ fast-glob:
+ specifier: ^3.3.3
+ version: 3.3.3
+ obug:
+ specifier: catalog:cli
+ version: 2.1.1
+ package-manager-detector:
+ specifier: ^1.6.0
+ version: 1.6.0
+ yaml:
+ specifier: ^2.8.2
+ version: 2.8.2
+ yargs:
+ specifier: catalog:cli
+ version: 18.0.0
+ zod:
+ specifier: catalog:schema
+ version: 4.2.1
+ devDependencies:
+ '@types/yargs':
+ specifier: 'catalog:'
+ version: 17.0.35
+ es-toolkit:
+ specifier: 'catalog:'
+ version: 1.43.0
+ jsr:
+ specifier: 'catalog:'
+ version: 0.13.5
+ tsdown:
+ specifier: 'catalog:'
+ version: 0.15.12(@arethetypeswrong/core@0.18.2)(oxc-resolver@11.14.2)(publint@0.3.15)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3))
+
core/prettier-plugin-handlebars:
devDependencies:
prettier:
@@ -5308,6 +5363,14 @@ packages:
babel-plugin-macros:
optional: true
+ dedent@1.7.1:
+ resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
+ peerDependencies:
+ babel-plugin-macros: ^3.1.0
+ peerDependenciesMeta:
+ babel-plugin-macros:
+ optional: true
+
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@@ -5475,18 +5538,6 @@ packages:
resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==}
engines: {node: '>=18'}
- dotenv-cli@8.0.0:
- resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==}
- hasBin: true
-
- dotenv-expand@10.0.0:
- resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
- engines: {node: '>=12'}
-
- dotenv@16.5.0:
- resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
- engines: {node: '>=12'}
-
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
@@ -5761,6 +5812,7 @@ packages:
eslint-plugin-zod-x@1.13.2:
resolution: {integrity: sha512-Zw3PkNvKDp2H3wfrSwh+ydbf6WtkKlMqFEzyAN+cxxueiZRj/AtJkdjmll2IFYnOSi+2VBVV9quqDUfZItcNLQ==}
engines: {node: ^20 || ^22 || >=24}
+ deprecated: eslint-plugin-zod-x is deprecated. Use eslint-plugin-zod >=3. See https://github.com/marcalexiei/eslint-plugin-zod/releases/tag/v3.0.0
peerDependencies:
eslint: ^9
zod: ^4
@@ -6868,6 +6920,10 @@ packages:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
+ jsr@0.13.5:
+ resolution: {integrity: sha512-qQP20ZcG28pYes7bCq3uuvixl1TL1EpJzwLPfoQadSyWk9j2AID66qhW8+aXpRDRFDvDkXFnONsSRhpnnQAupg==}
+ hasBin: true
+
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -7589,6 +7645,10 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+ node-stream-zip@1.15.0:
+ resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==}
+ engines: {node: '>=0.12.0'}
+
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -7831,12 +7891,12 @@ packages:
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
- package-manager-detector@1.3.0:
- resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
-
package-manager-detector@1.5.0:
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
@@ -8621,6 +8681,10 @@ packages:
search-insights@2.17.3:
resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==}
+ semiver@1.1.0:
+ resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==}
+ engines: {node: '>=6'}
+
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -10085,6 +10149,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
@@ -10257,11 +10322,6 @@ packages:
engines: {node: '>= 14'}
hasBin: true
- yaml@2.8.1:
- resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
- engines: {node: '>= 14.6'}
- hasBin: true
-
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
@@ -10354,9 +10414,6 @@ packages:
zod@4.0.15:
resolution: {integrity: sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ==}
- zod@4.1.12:
- resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
-
zod@4.2.1:
resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==}
@@ -15052,6 +15109,8 @@ snapshots:
dedent@1.7.0: {}
+ dedent@1.7.1: {}
+
deep-eql@5.0.2: {}
deep-extend@0.6.0: {}
@@ -15207,17 +15266,6 @@ snapshots:
dependencies:
type-fest: 4.41.0
- dotenv-cli@8.0.0:
- dependencies:
- cross-spawn: 7.0.6
- dotenv: 16.5.0
- dotenv-expand: 10.0.0
- minimist: 1.2.8
-
- dotenv-expand@10.0.0: {}
-
- dotenv@16.5.0: {}
-
dotenv@17.2.3: {}
dset@3.1.4: {}
@@ -17002,6 +17050,11 @@ snapshots:
jsonparse@1.3.1: {}
+ jsr@0.13.5:
+ dependencies:
+ node-stream-zip: 1.15.0
+ semiver: 1.1.0
+
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.9
@@ -17032,7 +17085,7 @@ snapshots:
smol-toml: 1.5.2
strip-json-comments: 5.0.3
typescript: 5.9.3
- zod: 4.1.12
+ zod: 4.2.1
kolorist@1.8.0: {}
@@ -17123,7 +17176,7 @@ snapshots:
nano-spawn: 2.0.0
pidtree: 0.6.0
string-argv: 0.3.2
- yaml: 2.8.1
+ yaml: 2.8.2
listr2@9.0.5:
dependencies:
@@ -18046,6 +18099,8 @@ snapshots:
node-releases@2.0.19: {}
+ node-stream-zip@1.15.0: {}
+
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
@@ -18353,10 +18408,10 @@ snapshots:
dependencies:
quansync: 0.2.10
- package-manager-detector@1.3.0: {}
-
package-manager-detector@1.5.0: {}
+ package-manager-detector@1.6.0: {}
+
pako@0.2.9: {}
param-case@2.1.1:
@@ -18698,7 +18753,7 @@ snapshots:
publint@0.3.15:
dependencies:
'@publint/pack': 0.1.2
- package-manager-detector: 1.3.0
+ package-manager-detector: 1.5.0
picocolors: 1.1.1
sade: 1.8.1
@@ -19282,6 +19337,8 @@ snapshots:
search-insights@2.17.3: {}
+ semiver@1.1.0: {}
+
semver@5.7.2: {}
semver@6.3.1: {}
@@ -20130,7 +20187,7 @@ snapshots:
markdown-it: 14.1.0
minimatch: 9.0.5
typescript: 5.9.3
- yaml: 2.8.1
+ yaml: 2.8.2
typesafe-path@0.2.2: {}
@@ -20971,8 +21028,6 @@ snapshots:
yaml@2.7.1: {}
- yaml@2.8.1: {}
-
yaml@2.8.2: {}
yargs-parser@21.1.1: {}
@@ -21068,8 +21123,6 @@ snapshots:
zod@4.0.15: {}
- zod@4.1.12: {}
-
zod@4.2.1: {}
zwitch@2.0.4: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index a9a14e72..63f2c8c7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -41,6 +41,8 @@ catalog:
"@catppuccin/palette": 1.7.1
"@iconify/types": ^2.0.0
"@iconify/utils": ^2.3.0
+ "@manypkg/find-root": "^3.1.0"
+ "@manypkg/get-packages": ^3.1.0
"@testing-library/react": ^16.3.0
"@testing-library/jest-dom": ^6.9.1
"@types/debug": ^4.1.12
@@ -54,6 +56,7 @@ catalog:
astro: 5.9.3
handlebars: 4.7.8
happy-dom: ^20.0.11
+ jsr: ^0.13.5
mdast: ^3.0.0
minify: 14.0.0
prettier: ^3.7.4