diff --git a/README.md b/README.md
index 927f78e9..0518192a 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Check the README for each package within the `packages` directory for specific u
| [rc](packages/rc/README.md) | ![npm](https://img.shields.io/npm/v/@anolilab/rc?style=flat-square&labelColor=292a44&color=663399&label=v) | This module provides a utility function to load rc configuration settings from various sources, including environment variables, default values, and configuration files located in multiple standard directories. It merges these settings into a single configuration object. | | |
| [semantic-release-pnpm](packages/semantic-release-pnpm/README.md) | ![npm](https://img.shields.io/npm/v/@anolilab/semantic-release-pnpm?style=flat-square&labelColor=292a44&color=663399&label=v) | Semantic-release plugin to publish a npm package with pnpm | | |
| [semantic-release-preset](packages/semantic-release-preset/README.md) | ![npm](https://img.shields.io/npm/v/@anolilab/semantic-release-preset?style=flat-square&labelColor=292a44&color=663399&label=v) | semantic-release is a fully automated version management and package publishing library |
+| [semantic-release-clean-package-json](packages/semantic-release-clean-package-json/README.md) | ![npm](https://img.shields.io/npm/v/@anolilab/semantic-release-clean-package-json?style=flat-square&labelColor=292a44&color=663399&label=v) | A semantic-release plugin to clean and optimize package.json files before publishing |
## How We Version
diff --git a/packages/semantic-release-clean-package-json/.releaserc.json b/packages/semantic-release-clean-package-json/.releaserc.json
index 6829aad5..8f187c0b 100644
--- a/packages/semantic-release-clean-package-json/.releaserc.json
+++ b/packages/semantic-release-clean-package-json/.releaserc.json
@@ -27,6 +27,8 @@
}
],
"@semantic-release/changelog",
+ "./dist/index.mjs",
+ "@anolilab/semantic-release-pnpm",
[
"@semantic-release/github",
{
@@ -39,8 +41,6 @@
{
"message": "chore(release): ${nextRelease.gitTag} [skip ci]\\n\\n${nextRelease.notes}"
}
- ],
- "./dist/index.mjs",
- "@anolilab/semantic-release-pnpm"
+ ]
]
}
diff --git a/packages/semantic-release-clean-package-json/CHANGELOG.md b/packages/semantic-release-clean-package-json/CHANGELOG.md
index 7def89ad..72814bc4 100644
--- a/packages/semantic-release-clean-package-json/CHANGELOG.md
+++ b/packages/semantic-release-clean-package-json/CHANGELOG.md
@@ -6,10 +6,10 @@
### Bug Fixes
-* changed plugin order to not publish cleaned package.json to github ([6673e69](https://github.com/anolilab/semantic-release/commit/6673e69723fd340380250fa2d986e4c37091351f))
+* changed plugin order to not publish cleaned package.json to GitHub ([6673e69](https://github.com/anolilab/semantic-release/commit/6673e69723fd340380250fa2d986e4c37091351f))
* remove usage of aggregate-error library ([e0c6e95](https://github.com/anolilab/semantic-release/commit/e0c6e95b07416d6694fa192ddca96e17a4c1c4b8))
* **semantic-release-clean-package-json:** fixed circular dependency ([93b51f9](https://github.com/anolilab/semantic-release/commit/93b51f9c03503e10f0c0a23dcd55273622b40aa0))
-* **semantic-release-clean-package-json:** removed cjs exports, semantic-release dont supports both types at the same time ([48c93c0](https://github.com/anolilab/semantic-release/commit/48c93c09cd5d6429b7658bc9a4fc573ea23462e2))
+* **semantic-release-clean-package-json:** removed cjs exports, semantic-release don't support both types at the same time ([48c93c0](https://github.com/anolilab/semantic-release/commit/48c93c09cd5d6429b7658bc9a4fc573ea23462e2))
### Dependencies
@@ -20,7 +20,7 @@
### Bug Fixes
-* changed plugin order to not publish cleaned package.json to github ([6673e69](https://github.com/anolilab/semantic-release/commit/6673e69723fd340380250fa2d986e4c37091351f))
+* changed plugin order to not publish cleaned package.json to GitHub ([6673e69](https://github.com/anolilab/semantic-release/commit/6673e69723fd340380250fa2d986e4c37091351f))
## @anolilab/semantic-release-clean-package-json 1.0.0-alpha.1 (2025-01-15)
diff --git a/packages/semantic-release-clean-package-json/README.md b/packages/semantic-release-clean-package-json/README.md
index 928145be..3bca45d5 100644
--- a/packages/semantic-release-clean-package-json/README.md
+++ b/packages/semantic-release-clean-package-json/README.md
@@ -1,7 +1,7 @@
anolilab semantic-release-clean-package-json
- Clean package.json before publish by removing unnecessary properties
+ A semantic-release plugin that cleans and optimizes package.json before publishing by removing unnecessary development and build-time properties
@@ -25,6 +25,16 @@
---
+## Why?
+
+When publishing packages to npm, many properties in `package.json` are only needed during development and build time, but not in the published package. This plugin automatically removes unnecessary properties while preserving essential ones needed for the package to work correctly in production.
+
+Key benefits:
+- Reduces package size by removing development-only properties
+- Prevents leaking internal configuration and metadata
+- Maintains a clean and focused package.json for end users
+- Customizable property preservation through configuration
+
## Install
```sh
@@ -44,16 +54,16 @@ pnpm add @anolilab/semantic-release-clean-package-json
The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration):
> [!IMPORTANT]
-> Very important: The plugin must be placed after the `@semantic-release/github` or `@semantic-release/git` and before `@anolilab/semantic-release-pnpm` or `@semantic-release/npm` plugin otherwise the `package.json` will be cleaned and published into GitHub / Your Git Provider.
+> Very important: The plugin must be placed before the `@semantic-release/github` or `@semantic-release/git` and before `@anolilab/semantic-release-pnpm` or `@semantic-release/npm` plugin otherwise the `package.json` will be cleaned and published into GitHub / Your Git Provider.
```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
- "@semantic-release/github",
"@anolilab/semantic-release-clean-package-json",
- "@anolilab/semantic-release-pnpm"
+ "@anolilab/semantic-release-pnpm",
+ "@semantic-release/github"
]
}
```
@@ -62,7 +72,8 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
| Step | Description |
| --------- | -------------------------------------------------------------------------------------------------------- |
-| `prepare` | Modifing the `package.json` file with the [default preserved properties](#default-preserved-properties). |
+| `publish` | - Creates a backup of the original package.json file
- Removes all non-preserved properties from package.json
- Keeps properties specified in the default list and custom `keep` option
- Preserves specific npm scripts if they are in the keep list
- Writes the cleaned package.json file |
+| `success` | - Restores the original package.json from backup
- Updates the version number to match the released version
- Removes the backup file
- Logs success or error messages |
### Options
@@ -77,6 +88,56 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
### Examples
+The plugin can be configured with custom properties to keep in addition to the default preserved ones:
+
+```json
+{
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ [
+ "@anolilab/semantic-release-clean-package-json",
+ {
+ "keep": ["custom field"]
+ }
+ ],
+ "@anolilab/semantic-release-pnpm",
+ "@semantic-release/github"
+ ]
+}
+```
+
+#### Example: Publishing a TypeScript Package
+
+When publishing a TypeScript package, you might want to keep TypeScript-specific fields:
+
+```jsonc
+{
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ [
+ "@anolilab/semantic-release-clean-package-json",
+ {
+ // This are the default values, just a example
+ "keep": [
+ "types",
+ "typings",
+ "typesVersions",
+ "module"
+ ]
+ }
+ ],
+ "@anolilab/semantic-release-pnpm",
+ "@semantic-release/github"
+ ]
+}
+```
+
+#### Example: Custom Package Root
+
+If your package.json is not in the root directory:
+
```json
{
"plugins": [
@@ -86,7 +147,7 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
[
"@anolilab/semantic-release-clean-package-json",
{
- "keep": ["custom filed"]
+ "pkgRoot": "dist"
}
],
"@anolilab/semantic-release-pnpm"
@@ -94,10 +155,6 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
}
```
-
-
-
-
### Default preserved properties
By default, these properties are preserved in `package.json`:
diff --git a/packages/semantic-release-clean-package-json/__tests__/index.test.ts b/packages/semantic-release-clean-package-json/__tests__/index.test.ts
index 1ec87264..fd6623de 100644
--- a/packages/semantic-release-clean-package-json/__tests__/index.test.ts
+++ b/packages/semantic-release-clean-package-json/__tests__/index.test.ts
@@ -4,8 +4,8 @@ import { readJson, writeJson } from "@visulima/fs";
import { temporaryDirectory } from "tempy";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { prepare } from "../src";
-import type { PrepareContext } from "../src/definitions/context";
+import { publish, success } from "../src";
+import type { CommonContext, PublishContext } from "../src/definitions/context";
const DEFAULT_PACKAGE_JSON = {
dependencies: {
@@ -22,9 +22,10 @@ const DEFAULT_PACKAGE_JSON = {
postinstall: "echo postinstall",
test: "echo test",
},
+ version: "1.0.0",
};
-const context: Partial = {
+const context: Partial = {
branch: {
name: "foo",
},
@@ -56,16 +57,18 @@ describe("semantic-release-clean-package-json", () => {
afterEach(async () => {
await rm(temporaryDirectoryPath, { recursive: true });
+
+ vi.resetAllMocks();
});
it("should removes unnecessary properties", async () => {
- expect.assertions(1);
+ expect.assertions(4);
const packageJsonPath = `${temporaryDirectoryPath}/package.json`;
await writeJson(packageJsonPath, DEFAULT_PACKAGE_JSON);
- await prepare({}, { cwd: temporaryDirectoryPath, ...context } as PrepareContext);
+ await publish({}, { cwd: temporaryDirectoryPath, ...context } as PublishContext);
await expect(readJson(packageJsonPath)).resolves.toStrictEqual({
dependencies: {
@@ -75,21 +78,25 @@ describe("semantic-release-clean-package-json", () => {
scripts: {
postinstall: "echo postinstall",
},
+ version: "1.0.0",
});
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith("Created a backup of the package.json file.");
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith('Removing property "devDependencies"');
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith('Removing property "eslintConfig"');
});
it("should keep flag from given config", async () => {
- expect.assertions(1);
+ expect.assertions(3);
const packageJsonPath = `${temporaryDirectoryPath}/package.json`;
await writeJson(packageJsonPath, DEFAULT_PACKAGE_JSON);
- await prepare(
+ await publish(
{
keep: ["eslintConfig", "devDependencies"],
},
- { cwd: temporaryDirectoryPath, ...context } as PrepareContext,
+ { cwd: temporaryDirectoryPath, ...context } as PublishContext,
);
await expect(readJson(packageJsonPath)).resolves.toStrictEqual({
@@ -106,6 +113,70 @@ describe("semantic-release-clean-package-json", () => {
scripts: {
postinstall: "echo postinstall",
},
+ version: "1.0.0",
+ });
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith("Created a backup of the package.json file.");
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith(
+ "Keeping the following properties: name, version, private, publishConfig, scripts.preinstall, scripts.install, scripts.postinstall, scripts.dependencies, files, bin, browser, main, man, jsdelivr, unpkg, dependencies, peerDependencies, peerDependenciesMeta, bundledDependencies, optionalDependencies, engines, os, cpu, description, keywords, author, contributors, license, homepage, repository, bugs, funding, type, exports, imports, sponsor, publisher, displayName, categories, galleryBanner, preview, contributes, activationEvents, badges, markdown, qna, extensionPack, extensionDependencies, extensionKind, icon, fesm2020, fesm2015, esm2020, es2020, types, typings, typesVersions, module, sideEffects, eslintConfig, devDependencies",
+ );
+ });
+
+ describe("success", () => {
+ it("should restore package.json from backup and update version", async () => {
+ expect.assertions(3);
+
+ const packageJsonPath = `${temporaryDirectoryPath}/package.json`;
+
+ // Create and write initial package.json
+ await writeJson(packageJsonPath, DEFAULT_PACKAGE_JSON);
+
+ // Run publish to create backup and modify package.json
+ await publish({}, { cwd: temporaryDirectoryPath, ...context } as PublishContext);
+
+ // Run success to restore from backup
+ await success({}, { ...context, cwd: temporaryDirectoryPath } as CommonContext);
+
+ // Verify the restored package.json
+ const restoredPackageJson = await readJson(packageJsonPath);
+
+ expect(restoredPackageJson).toStrictEqual(DEFAULT_PACKAGE_JSON); // This is just mocked without the pnpm or npm semantic-release plugin
+
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith("Restored modified package.json from backup.");
+ expect((context as PublishContext).logger.error).not.toHaveBeenCalled();
+ });
+
+ it("should log error when backup file is not found", async () => {
+ expect.assertions(2);
+
+ // Run success without creating backup first
+ await success({}, { cwd: temporaryDirectoryPath, ...context } as CommonContext);
+
+ expect((context as PublishContext).logger.error).toHaveBeenCalledWith("No backup package.json found.");
+ expect((context as PublishContext).logger.log).not.toHaveBeenCalledWith("Restored modified package.json from backup.");
+ });
+
+ it("should handle custom pkgRoot", async () => {
+ expect.assertions(3);
+
+ const customRoot = `${temporaryDirectoryPath}/dist`;
+ const packageJsonPath = `${customRoot}/package.json`;
+
+ // Create custom directory and package.json
+ await rm(customRoot, { force: true, recursive: true });
+ await writeJson(packageJsonPath, DEFAULT_PACKAGE_JSON);
+
+ // Run publish with custom pkgRoot
+ await publish({ pkgRoot: "dist" }, { cwd: temporaryDirectoryPath, ...context } as PublishContext);
+
+ // Run success with custom pkgRoot
+ await success({ pkgRoot: "dist" }, { cwd: temporaryDirectoryPath, ...context } as CommonContext);
+
+ // Verify the restored package.json
+ const restoredPackageJson = await readJson(packageJsonPath);
+ expect(restoredPackageJson).toStrictEqual(DEFAULT_PACKAGE_JSON); // This is just mocked without the pnpm or npm semantic-release plugin
+
+ expect((context as PublishContext).logger.log).toHaveBeenCalledWith("Restored modified package.json from backup.");
+ expect((context as PublishContext).logger.error).not.toHaveBeenCalled();
});
});
});
diff --git a/packages/semantic-release-clean-package-json/package.json b/packages/semantic-release-clean-package-json/package.json
index 43546448..727ec9a4 100644
--- a/packages/semantic-release-clean-package-json/package.json
+++ b/packages/semantic-release-clean-package-json/package.json
@@ -67,7 +67,7 @@
"devDependencies": {
"@anolilab/eslint-config": "^15.0.3",
"@anolilab/prettier-config": "^5.0.14",
- "@anolilab/semantic-release-pnpm": "1.1.8",
+ "@anolilab/semantic-release-pnpm": "1.1.9-alpha.1",
"@babel/core": "^7.26.0",
"@rushstack/eslint-plugin-security": "^0.8.3",
"@secretlint/secretlint-rule-preset-recommend": "^9.0.0",
diff --git a/packages/semantic-release-clean-package-json/src/definitions/context.ts b/packages/semantic-release-clean-package-json/src/definitions/context.ts
index ff378a63..731d0c11 100644
--- a/packages/semantic-release-clean-package-json/src/definitions/context.ts
+++ b/packages/semantic-release-clean-package-json/src/definitions/context.ts
@@ -50,8 +50,8 @@ type CommonContext4 = CommonContext3 & {
nextRelease: Release;
};
-// https://github.com/semantic-release/semantic-release/blob/27b105337b16dfdffb0dfa36d1178015e7ba68a3/index.js#L193
-export type PrepareContext = CommonContext4;
+// https://github.com/semantic-release/semantic-release/blob/27b105337b16dfdffb0dfa36d1178015e7ba68a3/index.js#L206
+export type PublishContext = CommonContext4;
// @todo infer return type from https://github.com/semantic-release/semantic-release/blob/27b105337b16dfdffb0dfa36d1178015e7ba68a3/lib/branches/index.js#L70
export interface BranchSpec {
diff --git a/packages/semantic-release-clean-package-json/src/index.ts b/packages/semantic-release-clean-package-json/src/index.ts
index a5f4add4..2a58f59c 100644
--- a/packages/semantic-release-clean-package-json/src/index.ts
+++ b/packages/semantic-release-clean-package-json/src/index.ts
@@ -1,21 +1,37 @@
-import { writeJson } from "@visulima/fs";
-import { join } from "@visulima/path";
+import { rm } from "node:fs/promises";
+
+import { isAccessible, readJson, writeJson } from "@visulima/fs";
+import { join, resolve } from "@visulima/path";
+import type { PackageJson } from "type-fest";
import defaultKeepProperties from "./default-keep-properties";
-import type { PrepareContext } from "./definitions/context";
+import type { CommonContext, PublishContext } from "./definitions/context";
import type { PluginConfig } from "./definitions/plugin-config";
import getPackage from "./utils/get-pkg";
-// eslint-disable-next-line sonarjs/cognitive-complexity,import/prefer-default-export
-export const prepare = async (pluginConfig: PluginConfig, context: PrepareContext): Promise => {
+// eslint-disable-next-line sonarjs/cognitive-complexity
+export const publish = async (pluginConfig: PluginConfig, context: PublishContext): Promise => {
const packageJson = await getPackage(pluginConfig, context);
+ const cwd = pluginConfig.pkgRoot ? resolve(context.cwd, pluginConfig.pkgRoot) : context.cwd;
+
+ await writeJson(
+ join(cwd, "package.json.back"),
+ packageJson,
+ {
+ detectIndent: true,
+ },
+ );
+
+ context.logger.log("Created a backup of the package.json file.");
const keepProperties = new Set([...defaultKeepProperties, ...(pluginConfig.keep ?? [])]);
context.logger.log(`Keeping the following properties: ${[...keepProperties].join(", ")}`);
+ const packageJsonCopy = { ...packageJson };
+
// eslint-disable-next-line no-loops/no-loops,no-restricted-syntax
- for (const property in packageJson) {
+ for (const property in packageJsonCopy) {
if (keepProperties.has(property)) {
// eslint-disable-next-line no-continue
continue;
@@ -23,17 +39,17 @@ export const prepare = async (pluginConfig: PluginConfig, context: PrepareContex
if (property === "scripts") {
// eslint-disable-next-line no-loops/no-loops,no-restricted-syntax
- for (const script in packageJson.scripts) {
+ for (const script in packageJsonCopy.scripts) {
if (keepProperties.has(`${property}.${script}`)) {
// eslint-disable-next-line no-continue
continue;
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete,security/detect-object-injection
- delete packageJson.scripts[script];
+ delete packageJsonCopy.scripts[script];
}
- if (packageJson.scripts && Object.keys(packageJson.scripts).length > 0) {
+ if (packageJsonCopy.scripts && Object.keys(packageJsonCopy.scripts).length > 0) {
// eslint-disable-next-line no-continue
continue;
}
@@ -41,10 +57,40 @@ export const prepare = async (pluginConfig: PluginConfig, context: PrepareContex
context.logger.log(`Removing property "${property}"`);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete,security/detect-object-injection
- delete packageJson[property];
+ delete packageJsonCopy[property];
}
- await writeJson(join(context.cwd, "package.json"), packageJson, {
+ await writeJson(join(cwd, "package.json"), packageJsonCopy, {
detectIndent: true,
});
};
+
+export const success = async (pluginConfig: PluginConfig, context: CommonContext): Promise => {
+ const cwd = pluginConfig.pkgRoot ? resolve(context.cwd, pluginConfig.pkgRoot) : context.cwd;
+
+ const backupPackageJson = join(cwd, "package.json.back");
+
+ if (await isAccessible(backupPackageJson)) {
+ const packageJson = await getPackage(pluginConfig, context);
+
+ const backupPackageJsonContent = (await readJson(backupPackageJson)) as PackageJson;
+
+ // Overwrite the version from the backup package.json
+ backupPackageJsonContent.version = packageJson.version;
+
+ await writeJson(
+ join(cwd, "package.json"),
+ backupPackageJsonContent,
+ {
+ detectIndent: true,
+ overwrite: true,
+ },
+ );
+
+ await rm(backupPackageJson);
+
+ context.logger.log("Restored modified package.json from backup.");
+ } else {
+ context.logger.error("No backup package.json found.");
+ }
+};