Skip to content

Commit

Permalink
feat(lint): check files are formatted
Browse files Browse the repository at this point in the history
Closes #102
Refs #3
  • Loading branch information
JamieMason committed Dec 30, 2023
1 parent 6cd7960 commit eda4dbb
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 119 deletions.
19 changes: 19 additions & 0 deletions site/src/content/docs/config/lint-formatting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: lintFormatting
---

When using `syncpack lint`, enables checking the formatting of `package.json` files matches the output of `syncpack format`.

## Default value

```json title=".syncpackrc"
{
"lintFormatting": true
}
```

:::note

There is no equivalent CLI Option for this configuration.

:::
19 changes: 19 additions & 0 deletions site/src/content/docs/config/lint-semver-ranges.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: lintSemverRanges
---

When using `syncpack lint`, enables checking that all the semver ranges in your `package.json` files are valid.

## Default value

```json title=".syncpackrc"
{
"lintSemverRanges": true
}
```

:::note

There is no equivalent CLI Option for this configuration.

:::
19 changes: 19 additions & 0 deletions site/src/content/docs/config/lint-versions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: lintVersions
---

When using `syncpack lint`, enables checking that all versions are valid.

## Default value

```json title=".syncpackrc"
{
"lintVersions": true
}
```

:::note

There is no equivalent CLI Option for this configuration.

:::
2 changes: 1 addition & 1 deletion src/bin-format/format.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Effect } from 'effect';
import { describe, expect, it } from 'vitest';
import { createScenario } from '../../test/lib/create-scenario';
import { format } from './format';
import { DEFAULT_CONFIG } from '../constants';
import { format } from './format';

describe('formatBugs', () => {
it('uses github shorthand format', async () => {
Expand Down
159 changes: 81 additions & 78 deletions src/bin-format/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import { isArray } from 'tightrope/guard/is-array';
import { isNonEmptyString } from 'tightrope/guard/is-non-empty-string';
import { isObject } from 'tightrope/guard/is-object';
import { getSortAz } from '../config/get-sort-az';
import { getSortExports } from '../config/get-sort-exports';
import { getSortFirst } from '../config/get-sort-first';
import { CliConfigTag } from '../config/tag';
import { type CliConfig } from '../config/types';
import type { ErrorHandlers } from '../error-handlers/default-error-handlers';
import { defaultErrorHandlers } from '../error-handlers/default-error-handlers';
import type { Ctx } from '../get-context';
import { getContext } from '../get-context';
import type { Io } from '../io';
import { IoTag } from '../io';
import { exitIfInvalid } from '../io/exit-if-invalid';
import { writeIfChanged } from '../io/write-if-changed';
import { withLogger } from '../lib/with-logger';
import { getSortExports } from '../config/get-sort-exports';

interface Input {
io: Io;
Expand All @@ -25,83 +26,7 @@ interface Input {
export function format({ io, cli, errorHandlers = defaultErrorHandlers }: Input) {
return pipe(
getContext({ io, cli, errorHandlers }),
Effect.map((ctx) => {
const { config, packageJsonFiles } = ctx;
const sortAz = getSortAz(config);
const sortExports = getSortExports(config);
const sortFirst = getSortFirst(config);
const sortPackages = config.rcFile.sortPackages !== false;
const formatBugs = config.rcFile.formatBugs !== false;
const formatRepository = config.rcFile.formatRepository !== false;

packageJsonFiles.forEach((file) => {
const { contents } = file.jsonFile;
const chain: any = contents;

if (formatBugs) {
const bugsUrl = chain?.bugs?.url;
if (bugsUrl) {
contents.bugs = bugsUrl;
}
}

if (formatRepository) {
const repoUrl = chain?.repository?.url;
const repoDir = chain?.repository?.directory;
if (isNonEmptyString(repoUrl) && !isNonEmptyString(repoDir)) {
contents.repository = repoUrl.includes('github.com')
? repoUrl.replace(/^.+github\.com\//, '')
: repoUrl;
}
}

if (sortExports.length > 0) {
visitExports(contents.exports);
}

if (sortAz.length > 0) {
sortAz.forEach((key) => sortAlphabetically(contents[key]));
}

if (sortPackages) {
const sortedKeys = Object.keys(contents).sort();
sortObject(sortedKeys, contents);
}

if (sortFirst.length > 0) {
const otherKeys = Object.keys(contents);
const sortedKeys = new Set([...sortFirst, ...otherKeys]);
sortObject(sortedKeys, contents);
}
});

return ctx;

function visitExports(value: unknown): void {
if (isObject(value)) {
const otherKeys = Object.keys(value);
const sortedKeys = new Set([...sortExports, ...otherKeys]);
sortObject(sortedKeys, value);
Object.values(value).forEach(visitExports);
}
}

function sortObject(sortedKeys: string[] | Set<string>, obj: Record<string, unknown>): void {
sortedKeys.forEach((key: string) => {
const value = obj[key];
delete obj[key];
obj[key] = value;
});
}

function sortAlphabetically(value: unknown): void {
if (isArray(value)) {
value.sort();
} else if (isObject(value)) {
sortObject(Object.keys(value).sort(), value);
}
}
}),
Effect.flatMap(pipeline),
Effect.flatMap((ctx) =>
pipe(
writeIfChanged(ctx),
Expand All @@ -121,3 +46,81 @@ export function format({ io, cli, errorHandlers = defaultErrorHandlers }: Input)
withLogger,
);
}

export function pipeline(ctx: Ctx): Effect.Effect<never, never, Ctx> {
const { config, packageJsonFiles } = ctx;
const sortAz = getSortAz(config);
const sortExports = getSortExports(config);
const sortFirst = getSortFirst(config);
const sortPackages = config.rcFile.sortPackages !== false;
const formatBugs = config.rcFile.formatBugs !== false;
const formatRepository = config.rcFile.formatRepository !== false;

packageJsonFiles.forEach((file) => {
const { contents } = file.jsonFile;
const chain: any = contents;

if (formatBugs) {
const bugsUrl = chain?.bugs?.url;
if (bugsUrl) {
contents.bugs = bugsUrl;
}
}

if (formatRepository) {
const repoUrl = chain?.repository?.url;
const repoDir = chain?.repository?.directory;
if (isNonEmptyString(repoUrl) && !isNonEmptyString(repoDir)) {
contents.repository = repoUrl.includes('github.com')
? repoUrl.replace(/^.+github\.com\//, '')
: repoUrl;
}
}

if (sortExports.length > 0) {
visitExports(sortExports, contents.exports);
}

if (sortAz.length > 0) {
sortAz.forEach((key) => sortAlphabetically(contents[key]));
}

if (sortPackages) {
const sortedKeys = Object.keys(contents).sort();
sortObject(sortedKeys, contents);
}

if (sortFirst.length > 0) {
const otherKeys = Object.keys(contents);
const sortedKeys = new Set([...sortFirst, ...otherKeys]);
sortObject(sortedKeys, contents);
}
});

return Effect.succeed(ctx);
}

function visitExports(sortExports: string[], value: unknown): void {
if (isObject(value)) {
const otherKeys = Object.keys(value);
const sortedKeys = new Set([...sortExports, ...otherKeys]);
sortObject(sortedKeys, value);
Object.values(value).forEach((nextValue) => visitExports(sortExports, nextValue));
}
}

function sortObject(sortedKeys: string[] | Set<string>, obj: Record<string, unknown>): void {
sortedKeys.forEach((key: string) => {
const value = obj[key];
delete obj[key];
obj[key] = value;
});
}

function sortAlphabetically(value: unknown): void {
if (isArray(value)) {
value.sort();
} else if (isObject(value)) {
sortObject(Object.keys(value).sort(), value);
}
}
48 changes: 44 additions & 4 deletions src/bin-lint/lint.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import chalk from 'chalk';
import { Context, Effect, pipe } from 'effect';
import { pipeline as format } from '../bin-format/format';
import { pipeline as lintSemverRanges } from '../bin-lint-semver-ranges/lint-semver-ranges';
import { pipeline as listMismatches } from '../bin-list-mismatches/list-mismatches';
import { CliConfigTag } from '../config/tag';
import { type CliConfig } from '../config/types';
import { ICON } from '../constants';
import type { ErrorHandlers } from '../error-handlers/default-error-handlers';
import { defaultErrorHandlers } from '../error-handlers/default-error-handlers';
import { getContext } from '../get-context';
import type { Io } from '../io';
import { IoTag } from '../io';
import { exitIfInvalid } from '../io/exit-if-invalid';
import { toJson } from '../io/to-json';
import { withLogger } from '../lib/with-logger';

interface Input {
Expand All @@ -21,10 +24,47 @@ interface Input {
export function lint({ io, cli, errorHandlers = defaultErrorHandlers }: Input) {
return pipe(
getContext({ io, cli, errorHandlers }),
Effect.tap(() => Effect.logInfo(chalk`{yellow Versions}`)),
Effect.flatMap((ctx) => listMismatches(ctx, io, errorHandlers)),
Effect.tap(() => Effect.logInfo(chalk`{yellow Semver Ranges}`)),
Effect.flatMap((ctx) => lintSemverRanges(ctx, io, errorHandlers)),
// Versions
Effect.flatMap((ctx) =>
Effect.gen(function* ($) {
if (ctx.config.rcFile.lintVersions !== false) {
yield* $(Effect.logInfo(chalk`{yellow Versions}`));
yield* $(listMismatches(ctx, io, errorHandlers));
}
return ctx;
}),
),
// Semver Ranges
Effect.flatMap((ctx) =>
Effect.gen(function* ($) {
if (ctx.config.rcFile.lintSemverRanges !== false) {
yield* $(Effect.logInfo(chalk`{yellow Semver Ranges}`));
yield* $(lintSemverRanges(ctx, io, errorHandlers));
}
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(Context.empty(), Context.add(CliConfigTag, cli), Context.add(IoTag, io))),
withLogger,
Expand Down
2 changes: 1 addition & 1 deletion src/config/get-filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isNonEmptyString } from 'tightrope/guard/is-non-empty-string';
import type { Ctx } from '../get-context';
import { DEFAULT_CONFIG } from '../constants';
import type { Ctx } from '../get-context';

export function getFilter({ cli, rcFile }: Ctx['config']): string {
// @TODO Deprecate `filter` in .syncpackrc
Expand Down
2 changes: 1 addition & 1 deletion src/config/get-indent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isNonEmptyString } from 'tightrope/guard/is-non-empty-string';
import type { Ctx } from '../get-context';
import { DEFAULT_CONFIG } from '../constants';
import type { Ctx } from '../get-context';

export function getIndent({ cli, rcFile }: Ctx['config']): string {
return isNonEmptyString(cli.indent)
Expand Down
2 changes: 1 addition & 1 deletion src/config/get-sort-az.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings';
import type { Ctx } from '../get-context';
import { isEmptyArray } from 'tightrope/guard/is-empty-array';
import { DEFAULT_CONFIG } from '../constants';
import type { Ctx } from '../get-context';

export function getSortAz({ rcFile }: Ctx['config']): string[] {
return isArrayOfStrings(rcFile.sortAz) || isEmptyArray(rcFile.sortAz)
Expand Down
6 changes: 6 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ export interface RcConfig {
formatRepository: boolean;
/** @see https://jamiemason.github.io/syncpack/config/indent */
indent: string;
/** @see https://jamiemason.github.io/syncpack/config/lint-formatting */
lintFormatting: boolean;
/** @see https://jamiemason.github.io/syncpack/config/lint-semver-ranges */
lintSemverRanges: boolean;
/** @see https://jamiemason.github.io/syncpack/config/lint-versions */
lintVersions: boolean;
/** @see https://jamiemason.github.io/syncpack/config/semver-groups */
semverGroups: SemverGroupConfig.Any[];
/** @see https://jamiemason.github.io/syncpack/config/sort-az */
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export const DEFAULT_CONFIG = {
formatBugs: true,
formatRepository: true,
indent: ' ',
lintFormatting: true,
lintSemverRanges: true,
lintVersions: true,
semverGroups: [],
sortAz: [
'bin',
Expand Down

0 comments on commit eda4dbb

Please sign in to comment.