Skip to content

Commit

Permalink
feat: natural-order as an optional rule
Browse files Browse the repository at this point in the history
  • Loading branch information
moritzjacobs committed Sep 18, 2020
1 parent a84ebe6 commit 2f1d1a5
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 23 deletions.
8 changes: 6 additions & 2 deletions .scriptlintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"strict": true
}
"strict": true,
"rules": {
"alphabetic-order": false,
"natural-order": true
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"other:selfupdate": "updtr",
"other:watch": "nodemon -e js,ts --watch src --exec 'run-p build:dev'",
"prepublishOnly": "run-p build",
"pretest": "run-s build",
"start": "node dist/index.js",
"pretest": "run-s build",
"test": "run-s test:exports test:lint test:types test:unit:ci test:self",
"test:exports": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts",
"test:lint": "eslint ./src ./tests --ext js,ts,tsx",
Expand Down Expand Up @@ -82,4 +82,4 @@
"cosmiconfig": "^7.0.0",
"detect-indent": "^6.0.0"
}
}
}
9 changes: 8 additions & 1 deletion src/defaultRuleSets.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import defaultRules from "./rules";
import { Rule } from "./types";

export const optionalRules = ["natural-order"];

const strict = defaultRules
.map((r: Rule) => r.name)
.filter((r) => !optionalRules.includes(r));

const ruleSets = {
default: [
"mandatory-test",
"mandatory-start",
"mandatory-dev",
"no-default-test",
],
strict: defaultRules.map((r: Rule) => r.name),
strict,
optionalRules,
};

export default ruleSets;
39 changes: 22 additions & 17 deletions src/loadRules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import defaultRuleSets from "./defaultRuleSets";
import defaultRuleSets, {optionalRules} from "./defaultRuleSets";
import defaultRules from "./rules";
// Types
import { Rule } from "./types";
Expand Down Expand Up @@ -32,31 +32,36 @@ const loadDefaultRulesFromSet = (strict: boolean): Array<Rule> => {
.filter((r): r is Rule => r !== null);
};

const loadOptionalRules = (): Array<Rule> => {
return optionalRules
.map((name: string) => getRuleByName(defaultRules, name))
.filter((r): r is Rule => r !== null);
};

export const loadRulesFromRuleConfig = (
strict: boolean,
rulesConfig?: RulesConfig,
customRules?: Array<Rule>
): Array<Rule> => {
const optionalRules = loadOptionalRules();
const rules = loadDefaultRulesFromSet(strict);

const loadedCustomRules =
rulesConfig && customRules
? customRules.filter((cr: Rule) => rulesConfig[cr.name])
: [];

const loadedRules = [...loadedCustomRules, ...rules];

// standard rulesets apply
if (!rulesConfig) {
return loadedRules;
return rules;
}

return loadedRules
.map((rule: Rule) => {
if (rule.name in rulesConfig && rulesConfig[rule.name] === false) {
return null;
}
// there's custom rules loaded
const loadedCustomRules = customRules
? customRules.filter((cr: Rule) => rulesConfig[cr.name])
: [];

return rule;
})
.filter((r): r is Rule => r !== null);
// there's enabled optional rules
const enabledOptionalRules = optionalRules.filter(
({ name }) => name in rulesConfig && Boolean(rulesConfig[name])
);

return [...loadedCustomRules, ...enabledOptionalRules, ...rules].filter(
({ name }) => !(name in rulesConfig) || rulesConfig[name] !== false
);
};
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import noAliases from "./no-aliases";
import prePostTriggerDefined from "./prepost-trigger-defined";
import usesAllowedNamespace from "./uses-allowed-namespace";
import alphabeticOrder from "./alphabetic-order";
import naturalOrder from "./natural-order";
import { Rule } from "../types";

const rules: Array<Rule> = [
Expand All @@ -18,6 +19,7 @@ const rules: Array<Rule> = [
usesAllowedNamespace,
prePostTriggerDefined,
alphabeticOrder,
naturalOrder,
makeForbidUnixOperators(/rm /, "rm -rf", "rimraf"),
makeForbidUnixOperators(
/ && /,
Expand Down
84 changes: 84 additions & 0 deletions src/rules/natural-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { PackageScripts } from "../types";
import { objectFromEntries } from "../utils";

type EntryInfo = {
order: number;
namespace: string;
entry: [string, string];
};

const prepareEntryInfo = (entry: [string, string]) => {
const name = entry[0];
const namespace = name.split(":")[0];

if (name.startsWith("pre")) {
return {
order: 0,
namespace: namespace.substr(3),
entry,
};
}
if (name.startsWith("post")) {
return {
order: 2,
namespace: namespace.substr(4),
entry,
};
}

return {
order: 1,
namespace,
entry,
};
};

export const sortScripts = (scripts: PackageScripts): PackageScripts => {
// prepare namespace and order info
const entries = Object.entries(scripts).map(prepareEntryInfo);

return objectFromEntries(
entries
// make unique namespace groups
.map((e: EntryInfo) => e.namespace)
.filter(
(name: string, i: number, a: Array<string>) =>
a.indexOf(name) === i
)
.sort()
.map((name: string) =>
entries.filter((e: EntryInfo) => e.namespace === name)
)
// sort inside the group
.map((group: Array<EntryInfo>) =>
// sort `1-title` vs `2-title` etc.
group.sort((a: EntryInfo, b: EntryInfo) =>
`${a.order}-${a.entry[0]}`.localeCompare(
`${b.order}-${b.entry[0]}`
)
)
)
// flatten array
.reduce(
(flatted: Array<EntryInfo>, group: Array<EntryInfo>) => [
...flatted,
...group,
],
[]
)
// reduce to entries again
.map((f: EntryInfo) => f.entry)
);
};

export default {
name: "natural-order",
isObjectRule: true,
message: "scripts must be in 'natural' order",
validate: (scripts: PackageScripts) => {
const sorted = sortScripts(scripts);

return Object.keys(sorted).join("|") === Object.keys(scripts).join("|");
},
fix: (scripts: PackageScripts) => sortScripts(scripts),
};
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,10 @@ export const patchScriptObjectEntry = (
return k === fromKey ? [toKey, value] : [k, v];
})
);

type ObjectFromEntries = {[key: string]: any};
export const objectFromEntries = <T>(iterable: Array<[string, T]>): ObjectFromEntries => iterable.reduce((obj, [key, value]) => {
obj[key] = value;

return obj
}, {} as ObjectFromEntries)
14 changes: 13 additions & 1 deletion tests/loadRules.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { loadRulesFromRuleConfig, getRuleByName } from "../src/loadRules";
import defaultRules from "../src/rules";
import {optionalRules} from "../src/defaultRuleSets";

describe("loadRules.ts", () => {
const defaultRulesLoaded = loadRulesFromRuleConfig(false);
Expand Down Expand Up @@ -28,8 +29,19 @@ describe("loadRules.ts", () => {
).toBe(customRule.name);
});

it("loads optional rules", () => {
const rulesWithCustomRule = loadRulesFromRuleConfig(
false,
{
"natural-order": true,
}
);

expect(rulesWithCustomRule.map(r => r.name)[0]).toBe("natural-order");
});

it("loads correct amount of rules", () => {
expect(strictRulesLoaded.length).toBe(defaultRules.length);
expect(strictRulesLoaded.length).toBe(defaultRules.length - optionalRules.length);
});

test("getRuleByName() nulls on unknown name", () => {
Expand Down
56 changes: 56 additions & 0 deletions tests/rules/natural-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import rule, { sortScripts } from "../../src/rules/natural-order";

describe("natural-order.ts", () => {
const scriptsUnsorted = {
postbuild: "foo",
"test:lint": "baz",
"build:cleanup": "bar",
"update:typings": "something",
test: "jest",
posttest: "baz",
pretest: "baz",
update: "foo",
"update:dependencies": "updtr",
prebuild: "foo",
build: "foo",
publish: "ok",
prepublishOnly: "else",
};

const scriptsSorted = {
prebuild: "foo",
build: "foo",
"build:cleanup": "bar",
postbuild: "foo",
publish: "ok",
prepublishOnly: "else",
pretest: "baz",
test: "jest",
"test:lint": "baz",
posttest: "baz",
update: "foo",
"update:dependencies": "updtr",
"update:typings": "something",
};

describe("validate()", () => {
it("should validate correctly", () => {
expect(rule.validate({})).toBe(true);
expect(rule.validate(scriptsUnsorted)).toBe(false);
expect(rule.validate(scriptsSorted)).toBe(true);
});
});

it("should fix issues", () => {
expect(rule.validate(rule.fix(scriptsUnsorted))).toBe(true);
});

describe("sortScripts()", () => {
it("should sort correctly", () => {
expect(sortScripts({})).toEqual({});
expect(Object.keys(sortScripts(scriptsUnsorted))).toEqual(
Object.keys(scriptsSorted)
);
});
});
});

0 comments on commit 2f1d1a5

Please sign in to comment.