From 2f1d1a5ca4317b02168c358cbd545dda33bd27eb Mon Sep 17 00:00:00 2001 From: Moritz Jacobs Date: Fri, 18 Sep 2020 12:50:37 +0200 Subject: [PATCH] feat: natural-order as an optional rule --- .scriptlintrc | 8 ++- package.json | 4 +- src/defaultRuleSets.ts | 9 +++- src/loadRules.ts | 39 +++++++------- src/rules/index.ts | 2 + src/rules/natural-order.ts | 84 +++++++++++++++++++++++++++++++ src/utils.ts | 7 +++ tests/loadRules.test.ts | 14 +++++- tests/rules/natural-order.test.ts | 56 +++++++++++++++++++++ 9 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 src/rules/natural-order.ts create mode 100644 tests/rules/natural-order.test.ts diff --git a/.scriptlintrc b/.scriptlintrc index b850461..6927002 100644 --- a/.scriptlintrc +++ b/.scriptlintrc @@ -1,3 +1,7 @@ { - "strict": true -} \ No newline at end of file + "strict": true, + "rules": { + "alphabetic-order": false, + "natural-order": true + } +} diff --git a/package.json b/package.json index e3ab29f..a13dd10 100644 --- a/package.json +++ b/package.json @@ -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", @@ -82,4 +82,4 @@ "cosmiconfig": "^7.0.0", "detect-indent": "^6.0.0" } -} +} \ No newline at end of file diff --git a/src/defaultRuleSets.ts b/src/defaultRuleSets.ts index 851a472..d9a32bb 100644 --- a/src/defaultRuleSets.ts +++ b/src/defaultRuleSets.ts @@ -1,6 +1,12 @@ 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", @@ -8,7 +14,8 @@ const ruleSets = { "mandatory-dev", "no-default-test", ], - strict: defaultRules.map((r: Rule) => r.name), + strict, + optionalRules, }; export default ruleSets; diff --git a/src/loadRules.ts b/src/loadRules.ts index 2d28126..1ebe448 100644 --- a/src/loadRules.ts +++ b/src/loadRules.ts @@ -1,4 +1,4 @@ -import defaultRuleSets from "./defaultRuleSets"; +import defaultRuleSets, {optionalRules} from "./defaultRuleSets"; import defaultRules from "./rules"; // Types import { Rule } from "./types"; @@ -32,31 +32,36 @@ const loadDefaultRulesFromSet = (strict: boolean): Array => { .filter((r): r is Rule => r !== null); }; +const loadOptionalRules = (): Array => { + return optionalRules + .map((name: string) => getRuleByName(defaultRules, name)) + .filter((r): r is Rule => r !== null); +}; + export const loadRulesFromRuleConfig = ( strict: boolean, rulesConfig?: RulesConfig, customRules?: Array ): Array => { + 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 + ); }; diff --git a/src/rules/index.ts b/src/rules/index.ts index 3fdae91..b106bf2 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -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 = [ @@ -18,6 +19,7 @@ const rules: Array = [ usesAllowedNamespace, prePostTriggerDefined, alphabeticOrder, + naturalOrder, makeForbidUnixOperators(/rm /, "rm -rf", "rimraf"), makeForbidUnixOperators( / && /, diff --git a/src/rules/natural-order.ts b/src/rules/natural-order.ts new file mode 100644 index 0000000..2b6e90b --- /dev/null +++ b/src/rules/natural-order.ts @@ -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) => + a.indexOf(name) === i + ) + .sort() + .map((name: string) => + entries.filter((e: EntryInfo) => e.namespace === name) + ) + // sort inside the group + .map((group: Array) => + // 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, group: Array) => [ + ...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), +}; diff --git a/src/utils.ts b/src/utils.ts index c12a3a1..551db4b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -74,3 +74,10 @@ export const patchScriptObjectEntry = ( return k === fromKey ? [toKey, value] : [k, v]; }) ); + +type ObjectFromEntries = {[key: string]: any}; +export const objectFromEntries = (iterable: Array<[string, T]>): ObjectFromEntries => iterable.reduce((obj, [key, value]) => { + obj[key] = value; + + return obj +}, {} as ObjectFromEntries) diff --git a/tests/loadRules.test.ts b/tests/loadRules.test.ts index 3309ac4..76de0cd 100644 --- a/tests/loadRules.test.ts +++ b/tests/loadRules.test.ts @@ -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); @@ -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", () => { diff --git a/tests/rules/natural-order.test.ts b/tests/rules/natural-order.test.ts new file mode 100644 index 0000000..4cb1d18 --- /dev/null +++ b/tests/rules/natural-order.test.ts @@ -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) + ); + }); + }); +});