Skip to content

Commit

Permalink
feat(core): preserve formatting when applying fixes
Browse files Browse the repository at this point in the history
Closes #241
Closes #195
  • Loading branch information
JamieMason committed Aug 25, 2024
1 parent aac4e24 commit 8322d1c
Show file tree
Hide file tree
Showing 17 changed files with 106 additions and 67 deletions.
9 changes: 0 additions & 9 deletions .zed/tasks.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
[
{
"label": "Add test for current file",
"command": "$ZED_WORKTREE_ROOT/node_modules/.bin/tsx",
"args": ["$ZED_WORKTREE_ROOT/scripts/add-test.ts", "$ZED_RELATIVE_FILE"],
"shell": "system",
"reveal": "never",
"use_new_terminal": true,
"hide": "always"
},
{
"label": "Vitest run",
"command": "$ZED_WORKTREE_ROOT/node_modules/.bin/vitest",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"enquirer": "2.4.1",
"fast-check": "3.21.0",
"globby": "14.0.2",
"jsonc-parser": "3.3.1",
"minimatch": "10.0.1",
"npm-package-arg": "11.0.3",
"ora": "8.0.1",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/bin-format/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { PackageJson } from '../get-package-json-files/package-json-file.js
import { exitIfInvalid } from '../io/exit-if-invalid.js';
import type { Io } from '../io/index.js';
import { IoTag } from '../io/index.js';
import { toFormattedJson } from '../io/to-formatted-json.js';
import { writeIfChanged } from '../io/write-if-changed.js';
import { withLogger } from '../lib/with-logger.js';

Expand Down Expand Up @@ -106,6 +107,8 @@ export function pipeline(ctx: Ctx): Effect.Effect<Ctx> {
const sortedKeys = new Set([...sortFirst, ...otherKeys]);
sortObject(sortedKeys, contents);
}

file.nextJson = toFormattedJson(ctx, file);
});

return Effect.succeed(ctx);
Expand Down
48 changes: 24 additions & 24 deletions src/bin-lint/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getContext } from '../get-context/index.js';
import { exitIfInvalid } from '../io/exit-if-invalid.js';
import type { Io } from '../io/index.js';
import { IoTag } from '../io/index.js';
import { toJson } from '../io/to-json.js';
import { toFormattedJson } from '../io/to-formatted-json.js';
import { withLogger } from '../lib/with-logger.js';

interface Input {
Expand All @@ -24,6 +24,29 @@ interface Input {
export function lint({ io, cli, errorHandlers = defaultErrorHandlers }: Input) {
return pipe(
getContext({ io, cli, errorHandlers }),
// Formatting
Effect.flatMap(ctx =>
Effect.gen(function* ($) {
if (ctx.config.rcFile.lintFormatting !== false) {
yield* $(Effect.logInfo(chalk`{yellow Formatting}`));
yield* $(format(ctx));
for (const file of ctx.packageJsonFiles) {
const shortPath = file.jsonFile.shortPath;
const formattedJson = toFormattedJson(ctx, file);
const isFormatted = file.jsonFile.json === formattedJson;
if (isFormatted) {
yield* $(
Effect.logInfo(chalk`{green ${ICON.tick}} ${shortPath}`),
);
} else {
ctx.isInvalid = true;
yield* $(Effect.logInfo(chalk`{red ${ICON.cross}} ${shortPath}`));
}
}
}
return ctx;
}),
),
// Versions
Effect.flatMap(ctx =>
Effect.gen(function* ($) {
Expand All @@ -44,29 +67,6 @@ export function lint({ io, cli, errorHandlers = defaultErrorHandlers }: Input) {
return ctx;
}),
),
// Formatting
Effect.flatMap(ctx =>
Effect.gen(function* ($) {
if (ctx.config.rcFile.lintFormatting !== false) {
yield* $(Effect.logInfo(chalk`{yellow Formatting}`));
yield* $(format(ctx));
for (const file of ctx.packageJsonFiles) {
const nextJson = toJson(ctx, file);
const hasChanged = file.jsonFile.json !== nextJson;
const shortPath = file.jsonFile.shortPath;
if (hasChanged) {
ctx.isInvalid = true;
yield* $(Effect.logInfo(chalk`{red ${ICON.cross}} ${shortPath}`));
} else {
yield* $(
Effect.logInfo(chalk`{green ${ICON.tick}} ${shortPath}`),
);
}
}
}
return ctx;
}),
),
Effect.flatMap(exitIfInvalid),
Effect.provide(
pipe(
Expand Down
8 changes: 7 additions & 1 deletion src/error-handlers/default-error-handlers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ describe('Broken Projects', () => {
await Effect.runPromiseExit(fixMismatches(scenario));
expect(scenario.errorHandlers.JsonParseError).toHaveBeenCalledWith({
_tag: 'JsonParseError',
error: expect.any(SyntaxError),
errors: expect.arrayContaining([
{
error: expect.any(Number),
length: expect.any(Number),
offset: expect.any(Number),
},
]),
filePath: expect.stringContaining('/package.json'),
json: '}INVALID{',
});
Expand Down
2 changes: 1 addition & 1 deletion src/error-handlers/default-error-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const defaultErrorHandlers: ErrorHandlers = {
[
chalk.red('An error was found when parsing a JSON file'),
chalk.red(' File:', err.filePath),
chalk.red(' Error:', err.error),
chalk.red(' Error:', err.errors),
].join(EOL),
);
},
Expand Down
9 changes: 8 additions & 1 deletion src/get-context/get-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@ it('errors when package.json is invalid', async () => {
},
})();
await Effect.runPromiseExit(pipe(getContext(scenario), Effect.merge));

expect(scenario.errorHandlers.JsonParseError).toHaveBeenCalledWith(
new JsonParseError({
error: expect.any(SyntaxError),
errors: expect.arrayContaining([
{
error: expect.any(Number),
length: expect.any(Number),
offset: expect.any(Number),
},
]),
filePath: expect.stringContaining('/package.json') as unknown as string,
json: 'THIS-IS-NOT-VALID-JSON',
}),
Expand Down
23 changes: 23 additions & 0 deletions src/get-package-json-files/package-json-file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Effect, pipe } from 'effect';
import { applyEdits, modify } from 'jsonc-parser';
import type { Strategy } from '../config/get-custom-types.js';
import type { Ctx } from '../get-context/index.js';
import { Instance } from '../get-instances/instance.js';
Expand Down Expand Up @@ -43,11 +44,15 @@ export class PackageJsonFile {
/** the .name property from the package.json file */
name: string | undefined;

/** the next package.json file contents after modification, with formatting preserved */
nextJson: string;

constructor(jsonFile: JsonFile<PackageJson>, config: Ctx['config']) {
this._instances = null;
this.config = config;
this.jsonFile = jsonFile;
this.name = jsonFile.contents.name;
this.nextJson = jsonFile.json;
}

getInstances(enabledTypes: Strategy.Any[]): Effect.Effect<Instance[]> {
Expand Down Expand Up @@ -87,4 +92,22 @@ export class PackageJsonFile {
}
return Effect.succeed(this._instances);
}

/**
* Apply an edit to the raw JSON string which will be written to disk. This string preserves the
* original formatting of the file.
*/
applyEdit(fullPath: string[], value: string | undefined): void {
const edits = modify(
this.nextJson,
fullPath.map(parseNumericStrings),
value,
{},
);
this.nextJson = applyEdits(this.nextJson, edits);
}
}

function parseNumericStrings(key: string): string | number {
return /[^0-9]/.test(key) ? key : Number(key);
}
16 changes: 9 additions & 7 deletions src/io/read-json-file-sync.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { dirname, relative } from 'node:path';
import { Data, Effect, pipe } from 'effect';
import { type ParseError, parse } from 'jsonc-parser';
import type { Io } from './index.js';
import type { ReadFileError } from './read-file-sync.js';
import { readFileSync } from './read-file-sync.js';

export class JsonParseError extends Data.TaggedClass('JsonParseError')<{
readonly error: unknown;
readonly errors: ParseError[];
readonly filePath: string;
readonly json: string;
}> {}
Expand All @@ -30,12 +31,13 @@ export function readJsonFileSync<T>(
return pipe(
Effect.Do,
Effect.bind('json', () => readFileSync(io, filePath)),
Effect.bind('contents', ({ json }) =>
Effect.try({
try: () => JSON.parse(json),
catch: error => new JsonParseError({ error, filePath, json }),
}),
),
Effect.bind('contents', ({ json }) => {
const errors: ParseError[] = [];
const data = parse(json, errors);
return errors.length === 0
? Effect.succeed(data)
: Effect.fail(new JsonParseError({ errors, filePath, json }));
}),
Effect.map(
({ contents, json }) =>
new JsonFile<T>({
Expand Down
2 changes: 1 addition & 1 deletion src/io/to-json.ts → src/io/to-formatted-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const newlines = {
},
};

export function toJson(ctx: Ctx, file: PackageJsonFile): string {
export function toFormattedJson(ctx: Ctx, file: PackageJsonFile): string {
const contents = file.jsonFile.contents;
const indent = getIndent(ctx.config);
const eol = newlines.detect(file.jsonFile.json);
Expand Down
32 changes: 11 additions & 21 deletions src/io/write-if-changed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ICON } from '../constants.js';
import type { Ctx } from '../get-context/index.js';
import type { PackageJsonFile } from '../get-package-json-files/package-json-file.js';
import type { Io } from './index.js';
import { toJson } from './to-json.js';
import type { WriteFileError } from './write-file-sync.js';
import { writeFileSync } from './write-file-sync.js';

Expand All @@ -14,27 +13,18 @@ export function writeIfChanged(
return pipe(
Effect.all(
ctx.packageJsonFiles.map((file: PackageJsonFile) =>
pipe(
Effect.Do,
Effect.bind('nextJson', () => Effect.succeed(toJson(ctx, file))),
Effect.bind('hasChanged', ({ nextJson }) =>
Effect.succeed(file.jsonFile.json !== nextJson),
),
Effect.flatMap(({ hasChanged, nextJson }) =>
hasChanged
? pipe(
writeFileSync(file.jsonFile.filePath, nextJson),
Effect.flatMap(() =>
Effect.logInfo(
chalk`{green ${ICON.tick}} ${file.jsonFile.shortPath}`,
),
),
)
: Effect.logInfo(
chalk`{dim ${ICON.skip} ${file.jsonFile.shortPath}}`,
file.jsonFile.json !== file.nextJson
? pipe(
writeFileSync(file.jsonFile.filePath, file.nextJson),
Effect.flatMap(() =>
Effect.logInfo(
chalk`{green ${ICON.tick}} ${file.jsonFile.shortPath}`,
),
),
),
),
)
: Effect.logInfo(
chalk`{dim ${ICON.skip} ${file.jsonFile.shortPath}}`,
),
),
),
Effect.map(() => ctx),
Expand Down
2 changes: 2 additions & 0 deletions src/strategy/name-and-version-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class NameAndVersionPropsStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[key] = nextValue;
file.applyEdit(fullPath, nextValue);
}),
),
Effect.tapError(() =>
Expand All @@ -93,6 +94,7 @@ export class NameAndVersionPropsStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[this.path] = nextValue;
file.applyEdit([this.path], nextValue);
}),
),
Effect.tapError(() =>
Expand Down
2 changes: 2 additions & 0 deletions src/strategy/named-version-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class NamedVersionStringStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[key] = nextValue;
file.applyEdit(fullPath, nextValue);
}),
),
Effect.tapError(() =>
Expand All @@ -79,6 +80,7 @@ export class NamedVersionStringStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[this.path] = nextValue;
file.applyEdit([this.path], nextValue);
}),
),
Effect.tapError(() =>
Expand Down
2 changes: 2 additions & 0 deletions src/strategy/unnamed-version-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class UnnamedVersionStringStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[key] = nextValue;
file.applyEdit(fullPath, nextValue);
}),
),
Effect.tapError(() =>
Expand All @@ -70,6 +71,7 @@ export class UnnamedVersionStringStrategy {
Effect.flatMap(parent =>
Effect.try(() => {
parent[this.path] = nextValue;
file.applyEdit([this.path], nextValue);
}),
),
Effect.tapError(() =>
Expand Down
4 changes: 3 additions & 1 deletion src/strategy/versions-by-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ export class VersionsByNameStrategy {
[name, version]: [string, string | Delete],
): Effect.Effect<PackageJsonFile> {
const nextValue = version === DELETE ? undefined : version;
const fullPath = this.path.split('.');
return pipe(
get(file.jsonFile.contents, ...this.path.split('.')),
get(file.jsonFile.contents, ...fullPath),
Effect.flatMap(getOptionOfNonEmptyObject),
Effect.flatMap(parent =>
Effect.try(() => {
parent[name] = nextValue;
file.applyEdit(fullPath.concat(name), nextValue);
}),
),
Effect.tapError(() =>
Expand Down
2 changes: 1 addition & 1 deletion test/lib/create-scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
PackageJsonFile,
} from '../../src/get-package-json-files/package-json-file.js';
import type { Io } from '../../src/io/index.js';
import { newlines } from '../../src/io/to-json.js';
import { newlines } from '../../src/io/to-formatted-json.js';
import type { Report } from '../../src/report.js';

type NodeFs = typeof fs;
Expand Down

0 comments on commit 8322d1c

Please sign in to comment.