mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-10-16 06:15:43 -04:00
fix: migrate JSON helpers to jsonc-parser for format-preserving in-place edits
- Replace hand-rolled `JSON.parse`/`JSON.stringify` round-trips in `json.ts` with `jsonc-parser` `modify` + `applyEdits` so comments, trailing commas, indentation, and key ordering are preserved across all `setJsonPath`, `unsetJsonPath`, `mergeJson`, and `addPackageDependency` calls - Add `setJsonPathSegments` / `unsetJsonPathSegments` for callers whose keys contain `.` (tsconfig `paths` aliases like `"@acme/ui.foo/*"` would be mis-split by the dot-path API); export both from `packages/codemods/src/index.ts` - Refactor `set-tsconfig-paths` to use the new segment-aware helpers and drop its local `parseJsonc` + `applyEdits` usage — the builtin no longer performs raw `fs.readFileSync`/`writeFileSync` itself - Add `jsonc-parser` to `apps/cli/package.json` (was already a transitive dep; now explicit) - Add `"format preservation (JSONC)"` test suite covering comment/trailing-comma survival, key-ordering stability under `addPackageDependency`, dotted-key segment addressing, and no-op behavior when a parent key is absent
This commit is contained in:
6 files changed
+200
-97
No files matched your search
@@ -49,6 +49,7 @@
|
|||||||
"@clack/prompts": "^1.4.0",
|
"@clack/prompts": "^1.4.0",
|
||||||
"citty": "^0.2.2",
|
"citty": "^0.2.2",
|
||||||
"handlebars": "^4.7.9",
|
"handlebars": "^4.7.9",
|
||||||
|
"jsonc-parser": "^3.3.1",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"semver": "^7.8.1",
|
"semver": "^7.8.1",
|
||||||
"ts-morph": "^28.0.0",
|
"ts-morph": "^28.0.0",
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { assertSafeRelativePath } from "@stanza/registry";
|
import { assertSafeRelativePath } from "@stanza/registry";
|
||||||
import { applyEdits, modify, parse, type ParseError, printParseErrorCode } from "jsonc-parser";
|
|
||||||
|
|
||||||
import type { Codemod } from "../index";
|
import {
|
||||||
|
type Codemod,
|
||||||
|
readJson,
|
||||||
|
setJsonPath,
|
||||||
|
setJsonPathSegments,
|
||||||
|
unsetJsonPath,
|
||||||
|
unsetJsonPathSegments,
|
||||||
|
} from "../index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge entries into a `tsconfig.json`'s `compilerOptions.paths` map.
|
* Merge entries into a `tsconfig.json`'s `compilerOptions.paths` map.
|
||||||
@@ -55,8 +61,7 @@ const setTsconfigPaths: Codemod<SetTsconfigPathsArgs> = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = fs.readFileSync(abs, "utf8");
|
const root = readRoot(abs);
|
||||||
const root = parseJsonc(text, rel);
|
|
||||||
const compilerOptions = isRecord(root.compilerOptions) ? root.compilerOptions : {};
|
const compilerOptions = isRecord(root.compilerOptions) ? root.compilerOptions : {};
|
||||||
const existing = isRecord(compilerOptions.paths) ? compilerOptions.paths : {};
|
const existing = isRecord(compilerOptions.paths) ? compilerOptions.paths : {};
|
||||||
|
|
||||||
@@ -91,12 +96,11 @@ const setTsconfigPaths: Codemod<SetTsconfigPathsArgs> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (compilerOptions.baseUrl === undefined) {
|
if (compilerOptions.baseUrl === undefined) {
|
||||||
text = applyEdits(text, modify(text, ["compilerOptions", "baseUrl"], ".", FORMAT));
|
setJsonPath(abs, "compilerOptions.baseUrl", ".");
|
||||||
}
|
}
|
||||||
for (const [key, val] of Object.entries(args.paths)) {
|
for (const [key, val] of Object.entries(args.paths)) {
|
||||||
text = applyEdits(text, modify(text, ["compilerOptions", "paths", key], val, FORMAT));
|
setJsonPathSegments(abs, ["compilerOptions", "paths", key], val);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(abs, text, "utf8");
|
|
||||||
ctx.claimRegion(rel, regionKeyFor(args));
|
ctx.claimRegion(rel, regionKeyFor(args));
|
||||||
return { touchedFiles: [rel] };
|
return { touchedFiles: [rel] };
|
||||||
},
|
},
|
||||||
@@ -109,43 +113,32 @@ const setTsconfigPaths: Codemod<SetTsconfigPathsArgs> = {
|
|||||||
return { touchedFiles: [] };
|
return { touchedFiles: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = fs.readFileSync(abs, "utf8");
|
const root = readRoot(abs);
|
||||||
const root = parseJsonc(text, rel);
|
|
||||||
const compilerOptions = isRecord(root.compilerOptions) ? root.compilerOptions : {};
|
const compilerOptions = isRecord(root.compilerOptions) ? root.compilerOptions : {};
|
||||||
const paths = isRecord(compilerOptions.paths) ? compilerOptions.paths : {};
|
const paths = isRecord(compilerOptions.paths) ? compilerOptions.paths : {};
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const [key, val] of Object.entries(args.paths)) {
|
for (const [key, val] of Object.entries(args.paths)) {
|
||||||
if (paths[key] !== undefined && arraysEqual(toArray(paths[key]), val)) {
|
if (paths[key] !== undefined && arraysEqual(toArray(paths[key]), val)) {
|
||||||
text = applyEdits(text, modify(text, ["compilerOptions", "paths", key], undefined, FORMAT));
|
unsetJsonPathSegments(abs, ["compilerOptions", "paths", key]);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
// If the paths map ends up empty after removal, drop it entirely.
|
// If the paths map ends up empty after removal, drop it entirely.
|
||||||
const after = parseJsonc(text, rel);
|
const after = readRoot(abs);
|
||||||
const afterCo = isRecord(after.compilerOptions) ? after.compilerOptions : {};
|
const afterCo = isRecord(after.compilerOptions) ? after.compilerOptions : {};
|
||||||
if (isRecord(afterCo.paths) && Object.keys(afterCo.paths).length === 0) {
|
if (isRecord(afterCo.paths) && Object.keys(afterCo.paths).length === 0) {
|
||||||
text = applyEdits(text, modify(text, ["compilerOptions", "paths"], undefined, FORMAT));
|
unsetJsonPath(abs, "compilerOptions.paths");
|
||||||
}
|
}
|
||||||
fs.writeFileSync(abs, text, "utf8");
|
|
||||||
}
|
}
|
||||||
ctx.releaseRegion(rel, regionKeyFor(args));
|
ctx.releaseRegion(rel, regionKeyFor(args));
|
||||||
return { touchedFiles: changed ? [rel] : [] };
|
return { touchedFiles: changed ? [rel] : [] };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const FORMAT = { formattingOptions: { tabSize: 2, insertSpaces: true } } as const;
|
function readRoot(file: string): Record<string, unknown> {
|
||||||
|
const v = readJson(file);
|
||||||
function parseJsonc(text: string, rel: string): Record<string, unknown> {
|
return isRecord(v) ? v : {};
|
||||||
const errors: ParseError[] = [];
|
|
||||||
const value = parse(text, errors, { allowTrailingComma: true });
|
|
||||||
if (errors.length > 0) {
|
|
||||||
const first = errors[0]!;
|
|
||||||
throw new Error(
|
|
||||||
`set-tsconfig-paths: ${rel} is not valid JSON/JSONC (${printParseErrorCode(first.error)} at offset ${first.offset}).`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return isRecord(value) ? value : {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveFilePath(
|
function resolveFilePath(
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export {
|
|||||||
mergeJson,
|
mergeJson,
|
||||||
setJsonPath,
|
setJsonPath,
|
||||||
unsetJsonPath,
|
unsetJsonPath,
|
||||||
|
setJsonPathSegments,
|
||||||
|
unsetJsonPathSegments,
|
||||||
addPackageDependency,
|
addPackageDependency,
|
||||||
removePackageDependency,
|
removePackageDependency,
|
||||||
addPackageScript,
|
addPackageScript,
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ import path from "node:path";
|
|||||||
|
|
||||||
import { describe, expect, it, beforeEach } from "vite-plus/test";
|
import { describe, expect, it, beforeEach } from "vite-plus/test";
|
||||||
|
|
||||||
import { addPackageDependency, addPackageScript, mergeJson, removePackageDependency } from "./json";
|
import {
|
||||||
|
addPackageDependency,
|
||||||
|
addPackageScript,
|
||||||
|
mergeJson,
|
||||||
|
removePackageDependency,
|
||||||
|
setJsonPath,
|
||||||
|
setJsonPathSegments,
|
||||||
|
unsetJsonPathSegments,
|
||||||
|
} from "./json";
|
||||||
|
|
||||||
let tmp: string;
|
let tmp: string;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -70,3 +78,53 @@ describe("addPackageScript", () => {
|
|||||||
expect(out.scripts["db:migrate"]).toBe("drizzle-kit migrate");
|
expect(out.scripts["db:migrate"]).toBe("drizzle-kit migrate");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("format preservation (JSONC)", () => {
|
||||||
|
it("keeps comments + trailing commas through a setJsonPath edit", () => {
|
||||||
|
const p = path.join(tmp, "tsconfig.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
p,
|
||||||
|
`{
|
||||||
|
// Editor hints
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true, // load-bearing
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
setJsonPath(p, "compilerOptions.target", "ES2022");
|
||||||
|
const text = fs.readFileSync(p, "utf8");
|
||||||
|
expect(text).toContain("// Editor hints");
|
||||||
|
expect(text).toContain("// load-bearing");
|
||||||
|
expect(text).toContain('"target": "ES2022"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves user key ordering when adding a dep", () => {
|
||||||
|
const p = writePkg({
|
||||||
|
name: "x",
|
||||||
|
// Intentional ordering — keys would be alphabetized by a JSON.stringify
|
||||||
|
// round-trip but jsonc-parser appends rather than rebuilds.
|
||||||
|
version: "0.0.0",
|
||||||
|
dependencies: { react: "^18", zustand: "^4" },
|
||||||
|
});
|
||||||
|
addPackageDependency(p, "better-auth", "^1.0.0");
|
||||||
|
const text = fs.readFileSync(p, "utf8");
|
||||||
|
expect(text.indexOf('"version"')).toBeLessThan(text.indexOf('"dependencies"'));
|
||||||
|
// New dep lands after the existing ones, not interleaved alphabetically.
|
||||||
|
expect(text.indexOf('"react"')).toBeLessThan(text.indexOf('"better-auth"'));
|
||||||
|
expect(text.indexOf('"zustand"')).toBeLessThan(text.indexOf('"better-auth"'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setJsonPathSegments handles keys containing `.` (tsconfig paths aliases)", () => {
|
||||||
|
const p = writePkg({ compilerOptions: { paths: {} } });
|
||||||
|
setJsonPathSegments(p, ["compilerOptions", "paths", "@acme/ui.next/*"], ["./src/*"]);
|
||||||
|
const out = JSON.parse(fs.readFileSync(p, "utf8"));
|
||||||
|
expect(out.compilerOptions.paths["@acme/ui.next/*"]).toEqual(["./src/*"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unsetJsonPathSegments no-ops on a missing parent key", () => {
|
||||||
|
const p = writePkg({ name: "x" });
|
||||||
|
// devDependencies doesn't exist — should not throw.
|
||||||
|
expect(() => unsetJsonPathSegments(p, ["devDependencies", "vitest"])).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
+117
-71
@@ -1,97 +1,140 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
import { applyEdits, modify, parse, type ParseError, printParseErrorCode } from "jsonc-parser";
|
||||||
|
|
||||||
/** Narrow an unknown JSON value to a plain object (excludes arrays and null). */
|
/** Narrow an unknown JSON value to a plain object (excludes arrays and null). */
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readJson(path: string): unknown {
|
const FORMAT = { formattingOptions: { tabSize: 2, insertSpaces: true } } as const;
|
||||||
return JSON.parse(fs.readFileSync(path, "utf8"));
|
|
||||||
|
function parseJsonc(text: string, file: string): unknown {
|
||||||
|
const errors: ParseError[] = [];
|
||||||
|
const value = parse(text, errors, { allowTrailingComma: true });
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const first = errors[0]!;
|
||||||
|
throw new Error(
|
||||||
|
`${file}: invalid JSON/JSONC (${printParseErrorCode(first.error)} at offset ${first.offset}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read a JSON file expected to hold an object, falling back to `{}` otherwise. */
|
function ensureTrailingNewline(s: string): string {
|
||||||
function readRecord(path: string): Record<string, unknown> {
|
return s.endsWith("\n") ? s : s + "\n";
|
||||||
const value = readJson(path);
|
|
||||||
return isRecord(value) ? value : {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeJson(path: string, value: unknown): void {
|
function readText(file: string): string {
|
||||||
fs.writeFileSync(path, JSON.stringify(value, null, 2) + "\n", "utf8");
|
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep-merge an object into the JSON at `path`. Arrays are replaced, not merged.
|
* Apply one surgical edit at a structured path. Preserves comments,
|
||||||
* Returns the dot-paths that were created or changed (for region claiming).
|
* trailing commas, indentation, and key ordering outside the edit point —
|
||||||
|
* the whole reason we use jsonc-parser instead of round-tripping through
|
||||||
|
* JSON.parse/stringify.
|
||||||
*/
|
*/
|
||||||
export function mergeJson(path: string, patch: Record<string, unknown>): string[] {
|
function modifyAtPath(file: string, path: (string | number)[], value: unknown): void {
|
||||||
|
const text = readText(file);
|
||||||
|
let edits;
|
||||||
|
try {
|
||||||
|
edits = modify(text, path, value, FORMAT);
|
||||||
|
} catch (err) {
|
||||||
|
// jsonc-parser throws "Can not delete in empty document" when asked to
|
||||||
|
// unset a path whose parent doesn't exist. That's a no-op for us.
|
||||||
|
if (value === undefined && err instanceof Error && /Can not delete/.test(err.message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (edits.length === 0) return;
|
||||||
|
fs.writeFileSync(file, ensureTrailingNewline(applyEdits(text, edits)), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readJson(file: string): unknown {
|
||||||
|
return parseJsonc(fs.readFileSync(file, "utf8"), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a JSON file expected to hold an object, falling back to `{}` otherwise. */
|
||||||
|
function readRecord(file: string): Record<string, unknown> {
|
||||||
|
if (!fs.existsSync(file)) return {};
|
||||||
|
const value = readJson(file);
|
||||||
|
return isRecord(value) ? value : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrite a JSON file end-to-end. Loses any existing comments/formatting,
|
||||||
|
* so prefer `setJsonPath` / `addPackageDependency` for in-place updates.
|
||||||
|
*/
|
||||||
|
export function writeJson(file: string, value: unknown): void {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep-merge `patch` into the JSON at `file`. Arrays replace (not merge).
|
||||||
|
* Returns the dot-paths that were created or changed (for region claiming).
|
||||||
|
*
|
||||||
|
* Walks the patch tree and emits one surgical edit per leaf change, so
|
||||||
|
* unchanged keys and any comments/trailing commas in the file survive.
|
||||||
|
*
|
||||||
|
* Caveat: dot-paths in the return value (and the path the edits target) are
|
||||||
|
* split on `.` — a key name like `"lodash.merge"` would be addressed as two
|
||||||
|
* segments. Callers that pass such keys should use `setJsonPath` with a
|
||||||
|
* dot-free leaf, or route through `addPackageDependency` (which uses a
|
||||||
|
* structured path internally).
|
||||||
|
*/
|
||||||
|
export function mergeJson(file: string, patch: Record<string, unknown>): string[] {
|
||||||
|
const existing = readRecord(file);
|
||||||
const touched: string[] = [];
|
const touched: string[] = [];
|
||||||
const merged = mergeRecurse(readRecord(path), patch, "", touched);
|
|
||||||
writeJson(path, merged);
|
const visit = (
|
||||||
|
target: Record<string, unknown>,
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
prefix: string,
|
||||||
|
): void => {
|
||||||
|
for (const [k, v] of Object.entries(source)) {
|
||||||
|
const dotPath = prefix ? `${prefix}.${k}` : k;
|
||||||
|
const here = target[k];
|
||||||
|
if (isRecord(v) && isRecord(here)) {
|
||||||
|
visit(here, v, dotPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (here !== v) {
|
||||||
|
touched.push(dotPath);
|
||||||
|
modifyAtPath(file, dotPath.split("."), v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(existing, patch, "");
|
||||||
return touched;
|
return touched;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeRecurse(
|
|
||||||
target: Record<string, unknown>,
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
prefix: string,
|
|
||||||
touched: string[],
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const out: Record<string, unknown> = { ...target };
|
|
||||||
for (const [k, v] of Object.entries(source)) {
|
|
||||||
const path = prefix ? `${prefix}.${k}` : k;
|
|
||||||
const existing = out[k];
|
|
||||||
if (isRecord(v) && isRecord(existing)) {
|
|
||||||
out[k] = mergeRecurse(existing, v, path, touched);
|
|
||||||
} else {
|
|
||||||
if (out[k] !== v) touched.push(path);
|
|
||||||
out[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setJsonPath(file: string, dotPath: string, value: unknown): void {
|
export function setJsonPath(file: string, dotPath: string, value: unknown): void {
|
||||||
const root = readRecord(file);
|
modifyAtPath(file, dotPath.split("."), value);
|
||||||
setPath(root, dotPath, value);
|
|
||||||
writeJson(file, root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unsetJsonPath(file: string, dotPath: string): void {
|
export function unsetJsonPath(file: string, dotPath: string): void {
|
||||||
const root = readRecord(file);
|
if (!fs.existsSync(file)) return;
|
||||||
unsetPath(root, dotPath);
|
modifyAtPath(file, dotPath.split("."), undefined);
|
||||||
writeJson(file, root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPath(root: Record<string, unknown>, dotPath: string, value: unknown): void {
|
/**
|
||||||
const parts = dotPath.split(".");
|
* Structured-path variants for callers whose keys may contain `.` (e.g.
|
||||||
const last = parts.pop();
|
* tsconfig `paths` aliases like `"@acme/ui.foo"`). The dot-path API would
|
||||||
// dotPath is non-empty by caller contract, so `parts` was non-empty.
|
* mis-split those into multiple segments.
|
||||||
if (last === undefined) return;
|
*/
|
||||||
let node = root;
|
export function setJsonPathSegments(
|
||||||
for (const p of parts) {
|
file: string,
|
||||||
const next = node[p];
|
segments: (string | number)[],
|
||||||
if (isRecord(next)) {
|
value: unknown,
|
||||||
node = next;
|
): void {
|
||||||
} else {
|
modifyAtPath(file, segments, value);
|
||||||
const created: Record<string, unknown> = {};
|
|
||||||
node[p] = created;
|
|
||||||
node = created;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node[last] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsetPath(root: Record<string, unknown>, dotPath: string): void {
|
export function unsetJsonPathSegments(file: string, segments: (string | number)[]): void {
|
||||||
const parts = dotPath.split(".");
|
if (!fs.existsSync(file)) return;
|
||||||
const last = parts.pop();
|
modifyAtPath(file, segments, undefined);
|
||||||
if (last === undefined) return;
|
|
||||||
let node = root;
|
|
||||||
for (const p of parts) {
|
|
||||||
const next = node[p];
|
|
||||||
if (!isRecord(next)) return;
|
|
||||||
node = next;
|
|
||||||
}
|
|
||||||
delete node[last];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addPackageDependency(
|
export function addPackageDependency(
|
||||||
@@ -101,14 +144,17 @@ export function addPackageDependency(
|
|||||||
options: { dev?: boolean } = {},
|
options: { dev?: boolean } = {},
|
||||||
): void {
|
): void {
|
||||||
const key = options.dev ? "devDependencies" : "dependencies";
|
const key = options.dev ? "devDependencies" : "dependencies";
|
||||||
setJsonPath(packageJsonPath, `${key}.${name}`, range);
|
// Structured path so dep names containing `.` (e.g. `lodash.merge`) stay
|
||||||
|
// a single segment instead of being interpreted as nested objects.
|
||||||
|
modifyAtPath(packageJsonPath, [key, name], range);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removePackageDependency(packageJsonPath: string, name: string): void {
|
export function removePackageDependency(packageJsonPath: string, name: string): void {
|
||||||
unsetJsonPath(packageJsonPath, `dependencies.${name}`);
|
if (!fs.existsSync(packageJsonPath)) return;
|
||||||
unsetJsonPath(packageJsonPath, `devDependencies.${name}`);
|
modifyAtPath(packageJsonPath, ["dependencies", name], undefined);
|
||||||
|
modifyAtPath(packageJsonPath, ["devDependencies", name], undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addPackageScript(packageJsonPath: string, name: string, command: string): void {
|
export function addPackageScript(packageJsonPath: string, name: string, command: string): void {
|
||||||
setJsonPath(packageJsonPath, `scripts.${name}`, command);
|
modifyAtPath(packageJsonPath, ["scripts", name], command);
|
||||||
}
|
}
|
||||||
Generated
+3
@@ -49,6 +49,9 @@ importers:
|
|||||||
handlebars:
|
handlebars:
|
||||||
specifier: ^4.7.9
|
specifier: ^4.7.9
|
||||||
version: 4.7.9
|
version: 4.7.9
|
||||||
|
jsonc-parser:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
picocolors:
|
picocolors:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
|
|||||||
Reference in new issue
Block a user