comment-variables 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/comments.config.js +79 -9
- package/library/_commons/constants/rules.js +9 -0
- package/library/_commons/rules/compress.js +9 -9
- package/library/_commons/rules/resolve.js +6 -6
- package/library/_commons/schemas/config.js +67 -0
- package/library/_commons/utilities/find-all-imports.js +31 -13
- package/library/_commons/utilities/flatten-config-data.js +107 -0
- package/library/_commons/utilities/flows.js +32 -24
- package/library/_commons/utilities/helpers.js +4 -2
- package/library/_commons/utilities/resolve-config.js +67 -0
- package/library/index.js +8 -25
- package/package.json +1 -1
- package/library/_commons/utilities/run-with-config.js +0 -177
package/comments.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const data = {
|
|
2
2
|
// for testing
|
|
3
3
|
levelOne: {
|
|
4
4
|
levelTwo: {
|
|
@@ -18,16 +18,86 @@ const config = {
|
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
// for deving
|
|
21
|
-
jsDoc: {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
jsDoc: Object.freeze({
|
|
22
|
+
definitions: Object.freeze({
|
|
23
|
+
exitDueToFailure:
|
|
24
|
+
"Terminates the whole process with a 'failure' code (1).",
|
|
25
|
+
escapeRegex: `Escapes all regex characters with a \`"\\"\` in a string to prepare it for use in a regex.`,
|
|
26
|
+
makeRuleResolve:
|
|
27
|
+
"The utility that creates the resolve rule based on the flattened config data, used to transform $COMMENT#* placeholders into actual comments.",
|
|
28
|
+
makeRuleCompress:
|
|
29
|
+
"The utility that creates the compress rule based on the reversed flattened config data, used to transform actual comments into $COMMENT#* placeholders.",
|
|
30
|
+
coreCommentsFlow:
|
|
31
|
+
"The core flow at the heart of resolving and compressing comments.",
|
|
32
|
+
resolveCommentsFlow:
|
|
33
|
+
"The flow that resolves $COMMENT#* placeholders intro actual comments.",
|
|
34
|
+
compressCommentsFlow:
|
|
35
|
+
"The flow that compresses actual comments into $COMMENT#* placeholders.",
|
|
36
|
+
findAllImports:
|
|
37
|
+
"Finds all import paths recursively related to a given file path.",
|
|
38
|
+
processImport: "Processes recursively and resolves a single import path.",
|
|
39
|
+
flattenConfigData:
|
|
40
|
+
"Flattens the config's data property into a one-dimensional object of $COMMENT-*-like keys and string values.",
|
|
41
|
+
resolveConfig:
|
|
42
|
+
"Verifies, validates and resolves the config path to retrieve the config's data and ignores.",
|
|
43
|
+
}),
|
|
44
|
+
params: Object.freeze({
|
|
45
|
+
string: "The string.",
|
|
46
|
+
flattenedConfigData:
|
|
47
|
+
"The flattened config data, with $COMMENT#* placeholders as keys and actual comments as values.",
|
|
48
|
+
reversedFlattenedConfigData:
|
|
49
|
+
"The reversed flattened config data, with actual comments as keys and $COMMENT#* placeholders as values.",
|
|
50
|
+
ruleName:
|
|
51
|
+
'The name of the rule currently used. (Either `"resolve"` or `"compress"`.)',
|
|
52
|
+
ignores:
|
|
53
|
+
"The array of paths and globs for the flow's ESLint instance to ignore.",
|
|
54
|
+
eitherFlattenedConfigData:
|
|
55
|
+
"Either the flattened config data or the reversed flattened config data, since they share the same structure.",
|
|
56
|
+
filePath:
|
|
57
|
+
"The absolute path of the file whose imports are being recursively found, such as that of a project's `comments.config.js` file.",
|
|
58
|
+
cwd: "The current working directory, set as `process.cwd()` by default.",
|
|
59
|
+
visitedSet:
|
|
60
|
+
"The set of strings tracking the import paths that have already been visited, instantiated as a `new Set()` by default.",
|
|
61
|
+
depth:
|
|
62
|
+
"The current depth of the recursion, instantiated at `0` by default.",
|
|
63
|
+
maxDepth:
|
|
64
|
+
"The maximum depth allowed for the recursion, instantiated at `100` by default.",
|
|
65
|
+
importPath: "The import path currently being addressed.",
|
|
66
|
+
currentDir:
|
|
67
|
+
"The directory containing the import path currently being addressed.",
|
|
68
|
+
configData:
|
|
69
|
+
"The config's data property. (Values are typed `any` given the limitations in typing recursive values in JSDoc.)",
|
|
70
|
+
configDataMap:
|
|
71
|
+
"The map housing the flattened keys with their values and sources through recursion, instantiated as a `new Map()`.",
|
|
72
|
+
parentKeys:
|
|
73
|
+
"The list of keys that are parent to the key at hand given the recursive nature of the config's data's data structure, instantiated as an empty array of strings.",
|
|
74
|
+
configPath:
|
|
75
|
+
"The path of the config, either from `comments.config.js` or from a config passed via the `--config` flag.",
|
|
76
|
+
}),
|
|
77
|
+
returns: Object.freeze({
|
|
78
|
+
exitDueToFailure:
|
|
79
|
+
"Never. (Somehow typing needs to be explicit for unreachable code inference.)",
|
|
80
|
+
escapeRegex: `The string with regex characters escaped.`,
|
|
81
|
+
makeRuleResolve: "The resolve rule based on the flattened config data.",
|
|
82
|
+
makeRuleCompress:
|
|
83
|
+
"The compress rule based on the reversed flattened config data.",
|
|
84
|
+
findAllImports:
|
|
85
|
+
"The complete set of strings of import paths recursively related to the given file path, or `null` if an issue has arisen.",
|
|
86
|
+
processImport:
|
|
87
|
+
"`true` to skip unresolved paths, `false` if resolution fails at any level.",
|
|
88
|
+
flattenConfigData:
|
|
89
|
+
"Both the flattened config data and its reversed version to ensure the strict reversibility of the `resolve` and `compress` commands.",
|
|
90
|
+
resolveConfig:
|
|
91
|
+
"The flattened config data, the reverse flattened config data, the verified config path and the raw passed ignores.",
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
24
94
|
};
|
|
25
95
|
|
|
26
|
-
|
|
96
|
+
const ignores = ["chocolat.js"];
|
|
27
97
|
|
|
28
|
-
const
|
|
29
|
-
data
|
|
30
|
-
ignores
|
|
98
|
+
const config = {
|
|
99
|
+
data,
|
|
100
|
+
ignores,
|
|
31
101
|
};
|
|
32
102
|
|
|
33
|
-
export default
|
|
103
|
+
export default config;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveRuleName, compressRuleName } from "../constants/bases.js";
|
|
2
|
+
|
|
3
|
+
import makeResolveRule from "../rules/resolve.js";
|
|
4
|
+
import makeCompressRule from "../rules/compress.js";
|
|
5
|
+
|
|
6
|
+
export const ruleNames_makeRules = Object.freeze({
|
|
7
|
+
[resolveRuleName]: makeResolveRule,
|
|
8
|
+
[compressRuleName]: makeCompressRule,
|
|
9
|
+
});
|
|
@@ -3,17 +3,17 @@ import { placeholderMessageId } from "../constants/bases.js";
|
|
|
3
3
|
import { escapeRegex } from "..//utilities/helpers.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param {{[key: string]: string}}
|
|
8
|
-
* @returns
|
|
6
|
+
* The utility that creates the compress rule based on the reversed flattened config data, used to transform actual comments into $COMMENT#* placeholders.
|
|
7
|
+
* @param {{[key: string]: string}} reversedFlattenedConfigData The reversed flattened config data, with actual comments as keys and $COMMENT#* placeholders as values.
|
|
8
|
+
* @returns The compress rule based on the reversed flattened config data.
|
|
9
9
|
*/
|
|
10
|
-
const makeRule = (
|
|
10
|
+
const makeRule = (reversedFlattenedConfigData) => {
|
|
11
11
|
// Turns the whole reversedFlattenedConfig from an object to an array of key-value arrays sorted by the descending length of each key to prevent partial replacements.
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const sortedReversedFlattenedConfigData = Object.entries(
|
|
13
|
+
reversedFlattenedConfigData
|
|
14
14
|
).sort(([a], [b]) => b.length - a.length);
|
|
15
15
|
|
|
16
|
-
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<
|
|
16
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<typeof placeholderMessageId, []>} */
|
|
17
17
|
const rule = {
|
|
18
18
|
meta: {
|
|
19
19
|
type: "suggestion",
|
|
@@ -28,7 +28,7 @@ const makeRule = (reversedFlattenedConfig) => {
|
|
|
28
28
|
fixable: "code",
|
|
29
29
|
schema: [],
|
|
30
30
|
},
|
|
31
|
-
create(context) {
|
|
31
|
+
create: (context) => {
|
|
32
32
|
const sourceCode = context.sourceCode;
|
|
33
33
|
const comments = sourceCode
|
|
34
34
|
.getAllComments()
|
|
@@ -41,7 +41,7 @@ const makeRule = (reversedFlattenedConfig) => {
|
|
|
41
41
|
for (const [
|
|
42
42
|
resolvedValue,
|
|
43
43
|
commentKey,
|
|
44
|
-
] of
|
|
44
|
+
] of sortedReversedFlattenedConfigData) {
|
|
45
45
|
const pattern = new RegExp(
|
|
46
46
|
`(?<=\\s|^)${escapeRegex(resolvedValue)}(?=\\s|$)`,
|
|
47
47
|
"g"
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
} from "../constants/bases.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {{[key: string]: string}}
|
|
9
|
-
* @returns
|
|
7
|
+
* The utility that creates the resolve rule based on the flattened config data, used to transform $COMMENT#* placeholders into actual comments.
|
|
8
|
+
* @param {{[key: string]: string}} flattenedConfigData The flattened config data, with $COMMENT#* placeholders as keys and actual comments as values.
|
|
9
|
+
* @returns The resolve rule based on the flattened config data.
|
|
10
10
|
*/
|
|
11
|
-
const makeRule = (
|
|
12
|
-
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<
|
|
11
|
+
const makeRule = (flattenedConfigData) => {
|
|
12
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<typeof placeholderMessageId, []>} */
|
|
13
13
|
const rule = {
|
|
14
14
|
meta: {
|
|
15
15
|
type: "suggestion",
|
|
@@ -43,7 +43,7 @@ const makeRule = (flattenedConfig) => {
|
|
|
43
43
|
for (const match of matches) {
|
|
44
44
|
const fullMatch = match[0]; // e.g. $COMMENT#LEVELONE#LEVELTWO
|
|
45
45
|
const key = match[1]; // e.g. LEVELONE#LEVELTWO
|
|
46
|
-
const replacement =
|
|
46
|
+
const replacement = flattenedConfigData[key];
|
|
47
47
|
|
|
48
48
|
if (replacement) {
|
|
49
49
|
fixedText = fixedText.replace(fullMatch, replacement);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { configKeyRegex } from "../constants/bases.js";
|
|
4
|
+
|
|
5
|
+
export const ConfigDataSchema = z
|
|
6
|
+
.lazy(() =>
|
|
7
|
+
z.record(
|
|
8
|
+
z.any().superRefine((val, ctx) => {
|
|
9
|
+
if (typeof val === "string") {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof val === "object" && val && !Array.isArray(val)) {
|
|
14
|
+
const parsed = ConfigDataSchema.safeParse(val);
|
|
15
|
+
if (!parsed.success) {
|
|
16
|
+
for (const issue of parsed.error.issues) {
|
|
17
|
+
ctx.addIssue({
|
|
18
|
+
...issue,
|
|
19
|
+
path: [...ctx.path, ...issue.path],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.addIssue({
|
|
27
|
+
code: z.ZodIssueCode.custom,
|
|
28
|
+
message: `Value \`${val}\` of type "${typeof val}" should be a string or a nested object.`,
|
|
29
|
+
path: ctx.path,
|
|
30
|
+
});
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
.superRefine((obj, ctx) => {
|
|
35
|
+
for (const key of Object.keys(obj)) {
|
|
36
|
+
if (key.includes("$")) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: z.ZodIssueCode.custom,
|
|
39
|
+
message: `Key "${key}" should not include the "$" character.`,
|
|
40
|
+
path: [key],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (key.includes("#")) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: z.ZodIssueCode.custom,
|
|
46
|
+
message: `Key "${key}" should not include the "#" character.`,
|
|
47
|
+
path: [key],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (!configKeyRegex.test(key)) {
|
|
51
|
+
ctx.addIssue({
|
|
52
|
+
code: z.ZodIssueCode.custom,
|
|
53
|
+
message: `Key "${key}" should only include whitespaces (s), lowercase letters (Ll), uppercase letters (Lu), other letters (Lo), numbers (N), dash punctuation (Pd), and connector punctuation (Pc).`,
|
|
54
|
+
path: [key],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const ConfigIgnoresSchema = z.array(
|
|
61
|
+
z.string({
|
|
62
|
+
message: `The config's "ignores" key array should be made of string or be empty.`,
|
|
63
|
+
}),
|
|
64
|
+
{
|
|
65
|
+
message: `The config's "ignores" key value should be an array of strings (or at the very least an empty array).`,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
@@ -7,50 +7,68 @@ import { getSourceCodeFromFilePath } from "get-sourcecode-from-file-path";
|
|
|
7
7
|
/* findAllImports */
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Processes recursively and resolves a single import path.
|
|
11
|
+
* @param {string} importPath The import path currently being addressed.
|
|
12
|
+
* @param {string} currentDir The directory containing the import path currently being addressed.
|
|
13
|
+
* @param {string} cwd The current working directory, set as `process.cwd()` by default.
|
|
14
|
+
* @param {Set<string>} visitedSet The set of strings tracking the import paths that have already been visited, instantiated as a `new Set()` by default.
|
|
15
|
+
* @param {number} depth The current depth of the recursion, instantiated at `0` by default.
|
|
16
|
+
* @param {number} maxDepth The maximum depth allowed for the recursion, instantiated at `100` by default.
|
|
17
|
+
* @returns `true` to skip unresolved paths, `false` if resolution fails at any level.
|
|
12
18
|
*/
|
|
13
19
|
const processImport = (
|
|
14
20
|
importPath,
|
|
15
21
|
currentDir,
|
|
16
22
|
cwd,
|
|
17
|
-
|
|
23
|
+
visitedSet,
|
|
18
24
|
depth,
|
|
19
25
|
maxDepth
|
|
20
26
|
) => {
|
|
21
27
|
const resolvedPath = resolveImportingPath(currentDir, importPath, cwd);
|
|
22
|
-
if (!resolvedPath) return true;
|
|
28
|
+
if (!resolvedPath) return true;
|
|
23
29
|
|
|
24
30
|
const result = findAllImports(
|
|
25
31
|
resolvedPath,
|
|
26
32
|
cwd,
|
|
27
|
-
|
|
33
|
+
visitedSet,
|
|
28
34
|
depth + 1,
|
|
29
35
|
maxDepth
|
|
30
36
|
);
|
|
31
37
|
return result !== null; // Returns false if child failed.
|
|
32
38
|
};
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Finds all import paths recursively related to a given file path.
|
|
42
|
+
* @param {string} filePath The absolute path of the file whose imports are being recursively found, such as that of a project's `comments.config.js` file.
|
|
43
|
+
* @param {string} cwd The current working directory, set as `process.cwd()` by default.
|
|
44
|
+
* @param {Set<string>} visitedSet The set of strings tracking the import paths that have already been visited, instantiated as a `new Set()` by default.
|
|
45
|
+
* @param {number} depth The current depth of the recursion, instantiated at `0` by default.
|
|
46
|
+
* @param {number} maxDepth The maximum depth allowed for the recursion, instantiated at `100` by default.
|
|
47
|
+
* @returns The complete set of strings of import paths recursively related to the given file path, or `null` if an issue has arisen.
|
|
48
|
+
*/
|
|
34
49
|
export const findAllImports = (
|
|
35
50
|
filePath,
|
|
36
51
|
cwd = process.cwd(),
|
|
37
|
-
|
|
52
|
+
visitedSet = new Set(),
|
|
38
53
|
depth = 0,
|
|
39
54
|
maxDepth = 100
|
|
40
55
|
) => {
|
|
41
|
-
//
|
|
56
|
+
// Fails early if max depth is recursively reached.
|
|
42
57
|
if (depth > maxDepth) {
|
|
43
58
|
console.error(`ERROR. Max depth ${maxDepth} reached at ${filePath}.`);
|
|
44
59
|
return null;
|
|
45
60
|
}
|
|
61
|
+
// Fails early if no file is found.
|
|
46
62
|
if (!fs.existsSync(filePath)) {
|
|
47
63
|
console.error(`ERROR. File not found at ${filePath}.`);
|
|
48
64
|
return null;
|
|
49
65
|
}
|
|
50
|
-
if (visited.has(filePath)) return visited;
|
|
51
66
|
|
|
52
|
-
//
|
|
53
|
-
|
|
67
|
+
// Updates the visited set.
|
|
68
|
+
if (visitedSet.has(filePath)) return visitedSet;
|
|
69
|
+
visitedSet.add(filePath);
|
|
70
|
+
|
|
71
|
+
// Parses the file's source code AST.
|
|
54
72
|
const sourceCode = getSourceCodeFromFilePath(filePath);
|
|
55
73
|
if (!sourceCode?.ast) {
|
|
56
74
|
console.error(`ERROR. Failed to parse AST for ${filePath}.`);
|
|
@@ -67,7 +85,7 @@ export const findAllImports = (
|
|
|
67
85
|
node.source.value,
|
|
68
86
|
currentDir,
|
|
69
87
|
cwd,
|
|
70
|
-
|
|
88
|
+
visitedSet,
|
|
71
89
|
depth,
|
|
72
90
|
maxDepth
|
|
73
91
|
)
|
|
@@ -88,7 +106,7 @@ export const findAllImports = (
|
|
|
88
106
|
node.expression.arguments[0].value,
|
|
89
107
|
currentDir,
|
|
90
108
|
cwd,
|
|
91
|
-
|
|
109
|
+
visitedSet,
|
|
92
110
|
depth,
|
|
93
111
|
maxDepth
|
|
94
112
|
)
|
|
@@ -98,5 +116,5 @@ export const findAllImports = (
|
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
return
|
|
119
|
+
return visitedSet; // success
|
|
102
120
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { flattenedConfigKeyRegex } from "../constants/bases.js";
|
|
2
|
+
|
|
3
|
+
import { exitDueToFailure } from "../utilities/helpers.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flattens the config's data property into a one-dimensional object of $COMMENT-*-like keys and string values.
|
|
7
|
+
* @param {Record<string, any>} configData The config's data property. (Values are typed `any` given the limitations in typing recursive values in JSDoc.)
|
|
8
|
+
* @param {Map<string, {value: string; source: string}>} configDataMap The map housing the flattened keys with their values and sources through recursion, instantiated as a `new Map()`.
|
|
9
|
+
* @param {string[]} parentKeys The list of keys that are parent to the key at hand given the recursive nature of the config's data's data structure, instantiated as an empty array of strings.
|
|
10
|
+
* @returns Both the flattened config data and its reversed version to ensure the strict reversibility of the `resolve` and `compress` commands.
|
|
11
|
+
*/
|
|
12
|
+
export const flattenConfigData = (
|
|
13
|
+
configData,
|
|
14
|
+
configDataMap = new Map(),
|
|
15
|
+
parentKeys = []
|
|
16
|
+
) => {
|
|
17
|
+
for (const [key, value] of Object.entries(configData)) {
|
|
18
|
+
const newKeys = [...parentKeys, key];
|
|
19
|
+
const normalizedKey = newKeys
|
|
20
|
+
.map((k) => k.toUpperCase())
|
|
21
|
+
.join("#")
|
|
22
|
+
.replace(/\s/g, "_");
|
|
23
|
+
const source = newKeys.join(" > ");
|
|
24
|
+
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
if (configDataMap.has(normalizedKey)) {
|
|
27
|
+
console.error(
|
|
28
|
+
`ERROR. The normalized key "${normalizedKey}" has already been assigned. Check between the two following key paths: \n"${
|
|
29
|
+
configDataMap.get(normalizedKey).source
|
|
30
|
+
}" \n"${source}"`
|
|
31
|
+
);
|
|
32
|
+
exitDueToFailure();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
configDataMap.set(normalizedKey, {
|
|
36
|
+
value,
|
|
37
|
+
source,
|
|
38
|
+
});
|
|
39
|
+
} else if (typeof value === "object" && value && !Array.isArray(value)) {
|
|
40
|
+
/** @type {Record<string, any>} */
|
|
41
|
+
const typedValue = value;
|
|
42
|
+
|
|
43
|
+
flattenConfigData(typedValue, configDataMap, newKeys);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// At this point we're out of the recursion, and we can start working with the complete data.
|
|
48
|
+
|
|
49
|
+
// strips metadata
|
|
50
|
+
/**@type {Map<string, string>} */
|
|
51
|
+
const map = new Map();
|
|
52
|
+
configDataMap.forEach((value, key) => {
|
|
53
|
+
map.set(key, value.value);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// makes the flattened config data object
|
|
57
|
+
const flattenedConfigData = Object.fromEntries(map);
|
|
58
|
+
|
|
59
|
+
// The integrity of the flattened config data needs to be established before working with it safely.
|
|
60
|
+
|
|
61
|
+
const flattenedConfigDataKeysSet = new Set(Object.keys(flattenedConfigData));
|
|
62
|
+
|
|
63
|
+
const flattenedConfigDataValuesArray = Object.values(flattenedConfigData);
|
|
64
|
+
const flattenedConfigDataValuesSet = new Set(flattenedConfigDataValuesArray);
|
|
65
|
+
|
|
66
|
+
flattenedConfigDataKeysSet.forEach((key) => {
|
|
67
|
+
// checks the reversability of flattenedConfigData
|
|
68
|
+
if (flattenedConfigDataValuesSet.has(key)) {
|
|
69
|
+
console.error(
|
|
70
|
+
`ERROR. The key "${key}" is and shouldn't be among the values of flattenedConfigData.`
|
|
71
|
+
);
|
|
72
|
+
exitDueToFailure();
|
|
73
|
+
}
|
|
74
|
+
if (!flattenedConfigKeyRegex.test(key)) {
|
|
75
|
+
// checks if each key for flattenedConfigData passes the flattenedConfigKeyRegex test
|
|
76
|
+
console.error(
|
|
77
|
+
`ERROR. Somehow the key "${key}" is not properly formatted. (This is mostly an internal mistake.)`
|
|
78
|
+
);
|
|
79
|
+
exitDueToFailure();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** @type {Set<string>} */
|
|
84
|
+
const set = new Set();
|
|
85
|
+
|
|
86
|
+
flattenedConfigDataValuesArray.forEach((value) => {
|
|
87
|
+
if (set.has(value)) {
|
|
88
|
+
// checks that no two values are duplicate
|
|
89
|
+
console.error(
|
|
90
|
+
`ERROR. The value "${value}" is already assigned to an existing key.`
|
|
91
|
+
);
|
|
92
|
+
exitDueToFailure();
|
|
93
|
+
}
|
|
94
|
+
set.add(value);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Also including the reversed flattened config data.
|
|
98
|
+
|
|
99
|
+
const reversedFlattenedConfigData = Object.fromEntries(
|
|
100
|
+
Object.entries(flattenedConfigData).map(([key, value]) => [value, key])
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
flattenedConfigData,
|
|
105
|
+
reversedFlattenedConfigData,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -10,16 +10,17 @@ import {
|
|
|
10
10
|
allMDVirtualJSTSFileGlobs,
|
|
11
11
|
typeScriptAndJSXCompatible,
|
|
12
12
|
} from "../constants/bases.js";
|
|
13
|
+
import { ruleNames_makeRules } from "../constants/rules.js";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
import makeCompressRule from "../rules/compress.js";
|
|
15
|
+
/* coreCommentsFlow */
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The core flow at the heart of resolving and compressing comments.
|
|
19
|
+
* @param {typeof resolveRuleName | typeof compressRuleName} ruleName The name of the rule currently used. (Either `"resolve"` or `"compress"`.)
|
|
20
|
+
* @param {string[]} ignores The array of paths and globs for the flow's ESLint instance to ignore.
|
|
21
|
+
* @param {{[key: string]: string}} flattenedConfigData Either the flattened config data or the reversed flattened config data, since they share the same structure.
|
|
22
|
+
*/
|
|
23
|
+
const coreCommentsFlow = async (ruleName, ignores, flattenedConfigData) => {
|
|
23
24
|
const eslint = new ESLint({
|
|
24
25
|
fix: true,
|
|
25
26
|
errorOnUnmatchedPattern: false,
|
|
@@ -32,12 +33,12 @@ const coreCommentsFlow = async (
|
|
|
32
33
|
plugins: {
|
|
33
34
|
[commentVariablesPluginName]: {
|
|
34
35
|
rules: {
|
|
35
|
-
[ruleName]:
|
|
36
|
+
[ruleName]: ruleNames_makeRules[ruleName](flattenedConfigData),
|
|
36
37
|
},
|
|
37
38
|
},
|
|
38
39
|
},
|
|
39
40
|
rules: {
|
|
40
|
-
[`${commentVariablesPluginName}/${ruleName}`]: "warn",
|
|
41
|
+
[`${commentVariablesPluginName}/${ruleName}`]: "warn",
|
|
41
42
|
},
|
|
42
43
|
},
|
|
43
44
|
{
|
|
@@ -78,25 +79,32 @@ const coreCommentsFlow = async (
|
|
|
78
79
|
}, 0);
|
|
79
80
|
|
|
80
81
|
console.log(
|
|
81
|
-
`✅ ${resolvedOrCompressed} ${total}
|
|
82
|
+
`✅ ${resolvedOrCompressed} comments on ${total} file${
|
|
83
|
+
total === 1 ? "" : "s"
|
|
84
|
+
}.`
|
|
82
85
|
);
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
/* resolveCommentsFlow */
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The flow that resolves $COMMENT#* placeholders intro actual comments.
|
|
92
|
+
* @param {string[]} ignores The array of paths and globs for the flow's ESLint instance to ignore.
|
|
93
|
+
* @param {{[key: string]: string}} flattenedConfigData The flattened config data, with $COMMENT#* placeholders as keys and actual comments as values.
|
|
94
|
+
* @returns
|
|
95
|
+
*/
|
|
85
96
|
export const resolveCommentsFlow = async (ignores, flattenedConfigData) =>
|
|
86
|
-
coreCommentsFlow(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
makeResolveRule,
|
|
90
|
-
flattenedConfigData
|
|
91
|
-
);
|
|
97
|
+
coreCommentsFlow(resolveRuleName, ignores, flattenedConfigData);
|
|
98
|
+
|
|
99
|
+
/* compressCommentsFlow */
|
|
92
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The flow that compresses actual comments into $COMMENT#* placeholders.
|
|
103
|
+
* @param {string[]} ignores The array of paths and globs for the flow's ESLint instance to ignore.
|
|
104
|
+
* @param {{[key: string]: string}} reversedFlattenedConfigData The reversed flattened config data, with actual comments as keys and $COMMENT#* placeholders as values.
|
|
105
|
+
* @returns
|
|
106
|
+
*/
|
|
93
107
|
export const compressCommentsFlow = async (
|
|
94
108
|
ignores,
|
|
95
109
|
reversedFlattenedConfigData
|
|
96
|
-
) =>
|
|
97
|
-
coreCommentsFlow(
|
|
98
|
-
compressRuleName,
|
|
99
|
-
ignores,
|
|
100
|
-
makeCompressRule,
|
|
101
|
-
reversedFlattenedConfigData
|
|
102
|
-
);
|
|
110
|
+
) => coreCommentsFlow(compressRuleName, ignores, reversedFlattenedConfigData);
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Terminates the whole process with a 'failure' code (1).
|
|
5
|
-
* @returns Never.
|
|
5
|
+
* @returns {never} Never. (Somehow typing needs to be explicit for unreachable code inference.)
|
|
6
6
|
*/
|
|
7
7
|
export const exitDueToFailure = () => process.exit(1);
|
|
8
8
|
|
|
9
|
+
/* escapeRegex */
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
|
-
* Escapes all regex characters with a `"\"` in a string to prepare it for use in a regex
|
|
12
|
+
* Escapes all regex characters with a `"\"` in a string to prepare it for use in a regex.
|
|
11
13
|
* @param {string} string The string.
|
|
12
14
|
* @returns The string with regex characters escaped.
|
|
13
15
|
*/
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
|
|
4
|
+
import { flattenConfigData } from "./flatten-config-data.js";
|
|
5
|
+
|
|
6
|
+
import { ConfigDataSchema, ConfigIgnoresSchema } from "../schemas/config.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Verifies, validates and resolves the config path to retrieve the config's data and ignores.
|
|
10
|
+
* @param {string} configPath The path of the config, either from `comments.config.js` or from a config passed via the `--config` flag.
|
|
11
|
+
* @returns The flattened config data, the reverse flattened config data, the verified config path and the raw passed ignores.
|
|
12
|
+
*/
|
|
13
|
+
export async function resolveConfig(configPath) {
|
|
14
|
+
// Step 1: Checks if config file exists
|
|
15
|
+
|
|
16
|
+
if (!existsSync(configPath)) {
|
|
17
|
+
console.warn("No config file found. Exiting gracefully.");
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Step 2: Imports the config dynamically
|
|
22
|
+
|
|
23
|
+
const configModule = await import(pathToFileURL(configPath));
|
|
24
|
+
const config = configModule.default;
|
|
25
|
+
|
|
26
|
+
// Step 3: Validates config object
|
|
27
|
+
|
|
28
|
+
// validates config
|
|
29
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
30
|
+
console.warn(
|
|
31
|
+
"Invalid config format. The config should be an object. Exiting."
|
|
32
|
+
);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// validates config.data
|
|
37
|
+
const configDataResult = ConfigDataSchema.safeParse(config.data);
|
|
38
|
+
|
|
39
|
+
if (!configDataResult.success) {
|
|
40
|
+
console.warn("Config data could not pass validation from zod.");
|
|
41
|
+
configDataResult.error.errors.map((e) => console.log(e.message));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// validates config.ignores
|
|
46
|
+
const configIgnoresSchemaResult = ConfigIgnoresSchema.safeParse(
|
|
47
|
+
config.ignores
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!configIgnoresSchemaResult.success) {
|
|
51
|
+
console.warn("Config ignores could not pass validation from zod.");
|
|
52
|
+
configIgnoresSchemaResult.error.errors.map((e) => console.log(e.message));
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// sends back:
|
|
57
|
+
// - the flattened config data,
|
|
58
|
+
// - the reverse flattened config data,
|
|
59
|
+
// - the verified config path
|
|
60
|
+
// - and the raw passed ignores
|
|
61
|
+
console.log("Running with config:", config);
|
|
62
|
+
return {
|
|
63
|
+
...flattenConfigData(configDataResult.data), // finalized
|
|
64
|
+
configPath, // finalized
|
|
65
|
+
passedIgnores: configIgnoresSchemaResult.data, // addressed with --lint-config-imports and --my-ignores-only to be finalized
|
|
66
|
+
};
|
|
67
|
+
}
|
package/library/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./_commons/constants/bases.js";
|
|
16
16
|
|
|
17
17
|
import { exitDueToFailure } from "./_commons/utilities/helpers.js";
|
|
18
|
-
import {
|
|
18
|
+
import { resolveConfig } from "./_commons/utilities/resolve-config.js";
|
|
19
19
|
import { findAllImports } from "./_commons/utilities/find-all-imports.js";
|
|
20
20
|
|
|
21
21
|
import {
|
|
@@ -56,8 +56,10 @@ const passedConfigPath =
|
|
|
56
56
|
// defaults to comments.config.js if no --config flag is set
|
|
57
57
|
const rawConfigPath = passedConfigPath ?? path.join(cwd, defaultConfigFileName);
|
|
58
58
|
|
|
59
|
-
const results = await
|
|
60
|
-
if (!results)
|
|
59
|
+
const results = await resolveConfig(rawConfigPath);
|
|
60
|
+
if (!results) {
|
|
61
|
+
exitDueToFailure();
|
|
62
|
+
}
|
|
61
63
|
|
|
62
64
|
const {
|
|
63
65
|
flattenedConfigData,
|
|
@@ -71,26 +73,12 @@ console.log("Reversed flattened config is:", reversedFlattenedConfigData);
|
|
|
71
73
|
console.log("Config path is:", configPath);
|
|
72
74
|
console.log("Passed ignores are:", passedIgnores);
|
|
73
75
|
|
|
74
|
-
// VALIDATES THE REVERSABILITY OF THE CONCEIVED flattenedConfigData
|
|
75
|
-
|
|
76
|
-
const keys = new Set([...Object.keys(flattenedConfigData)]);
|
|
77
|
-
const values = new Set([...Object.values(flattenedConfigData)]);
|
|
78
|
-
|
|
79
|
-
keys.forEach((key) => {
|
|
80
|
-
if (values.has(key)) {
|
|
81
|
-
console.error(
|
|
82
|
-
`The key "${key}" is and shouldn't be among the values of flattenedConfigData.`
|
|
83
|
-
);
|
|
84
|
-
exitDueToFailure();
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
76
|
// ADDRESSES THE --lint-config-imports FLAG, GIVEN THAT THE FILES IMPORTED BY THE CONFIG ARE IGNORED BY DEFAULT.
|
|
89
77
|
|
|
90
78
|
const lintConfigImports = commands.indexOf(lintConfigImportsFlag) >= 2;
|
|
91
79
|
const rawConfigPathIgnores = lintConfigImports
|
|
92
80
|
? [configPath]
|
|
93
|
-
: [...findAllImports(configPath)];
|
|
81
|
+
: [...(findAllImports(configPath) ?? [])];
|
|
94
82
|
|
|
95
83
|
// the ignore paths must be relative
|
|
96
84
|
const configPathIgnores = rawConfigPathIgnores.map((e) =>
|
|
@@ -124,8 +112,8 @@ switch (coreCommand) {
|
|
|
124
112
|
case undefined: // falls through the default
|
|
125
113
|
default:
|
|
126
114
|
if (coreCommand && !coreCommand.startsWith("--"))
|
|
127
|
-
console.
|
|
128
|
-
`Core command not recognized. Choose between "resolve" and "compress".`
|
|
115
|
+
console.error(
|
|
116
|
+
`ERROR. Core command not recognized. Choose between "resolve" and "compress".`
|
|
129
117
|
);
|
|
130
118
|
else
|
|
131
119
|
console.log(
|
|
@@ -137,8 +125,3 @@ switch (coreCommand) {
|
|
|
137
125
|
);
|
|
138
126
|
break;
|
|
139
127
|
}
|
|
140
|
-
|
|
141
|
-
/* Notes
|
|
142
|
-
I'm going to have to redo this, but for now I just want to vibe code it in order to see how it is possible to make this.
|
|
143
|
-
Edit: Code vibing, not vibe coding. That's what I did here.
|
|
144
|
-
*/
|
package/package.json
CHANGED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { pathToFileURL } from "url";
|
|
3
|
-
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
import { configKeyRegex } from "../constants/bases.js";
|
|
7
|
-
|
|
8
|
-
function flattenConfigData(
|
|
9
|
-
configData,
|
|
10
|
-
normalizedPath = "",
|
|
11
|
-
map = {},
|
|
12
|
-
pathStack = [],
|
|
13
|
-
reversedFlattenedConfigData = {}
|
|
14
|
-
) {
|
|
15
|
-
for (const [key, value] of Object.entries(configData)) {
|
|
16
|
-
const currentPath = [...pathStack, key];
|
|
17
|
-
normalizedPath = currentPath
|
|
18
|
-
.map((k) => k.toUpperCase())
|
|
19
|
-
.join("#")
|
|
20
|
-
.replace(/\s/g, "_"); // whitespaces are replaced by underscores
|
|
21
|
-
|
|
22
|
-
if (typeof value === "string") {
|
|
23
|
-
if (map[normalizedPath]) {
|
|
24
|
-
// checks that no two keys are duplicate
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Duplicate normalized key detected: "${normalizedPath}".\nConflict between:\n - ${
|
|
27
|
-
map[normalizedPath].__source
|
|
28
|
-
}\n - ${currentPath.join(" > ")}`
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
map[normalizedPath] = {
|
|
32
|
-
value,
|
|
33
|
-
__source: currentPath.join(" > "), // for debugging
|
|
34
|
-
};
|
|
35
|
-
} else if (typeof value === "object") {
|
|
36
|
-
flattenConfigData(value, normalizedPath, map, currentPath);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const flattenedConfigData = Object.fromEntries(
|
|
41
|
-
Object.entries(map).map(([k, v]) => [k, v.value])
|
|
42
|
-
); // strip metadata
|
|
43
|
-
|
|
44
|
-
const set = new Set();
|
|
45
|
-
|
|
46
|
-
// the integrity of the config needs to be established before working with it
|
|
47
|
-
for (const value of Object.values(flattenedConfigData)) {
|
|
48
|
-
if (set.has(value)) {
|
|
49
|
-
// checks that no two values are duplicate
|
|
50
|
-
throw new Error(
|
|
51
|
-
`Value "${value}" is already assigned to an existing key.`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
set.add(value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const [key, value] of Object.entries(flattenedConfigData)) {
|
|
58
|
-
reversedFlattenedConfigData[value] = key;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
flattenedConfigData,
|
|
63
|
-
reversedFlattenedConfigData,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function runWithConfig(configPath) {
|
|
68
|
-
// Step 1: Check if config file exists
|
|
69
|
-
if (!existsSync(configPath)) {
|
|
70
|
-
console.warn("No config file found. Exiting gracefully.");
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Step 2: Import the config dynamically
|
|
75
|
-
const configModule = await import(pathToFileURL(configPath));
|
|
76
|
-
const config = configModule.default;
|
|
77
|
-
|
|
78
|
-
// Step 3: Validate config object
|
|
79
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
80
|
-
console.warn(
|
|
81
|
-
"Invalid config format. The config should be an object. Exiting."
|
|
82
|
-
);
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// data
|
|
87
|
-
|
|
88
|
-
const RecursiveObject = z
|
|
89
|
-
.lazy(() =>
|
|
90
|
-
z.record(
|
|
91
|
-
z.any().superRefine((val, ctx) => {
|
|
92
|
-
if (typeof val === "string") {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
97
|
-
const parsed = RecursiveObject.safeParse(val);
|
|
98
|
-
if (!parsed.success) {
|
|
99
|
-
for (const issue of parsed.error.issues) {
|
|
100
|
-
ctx.addIssue({
|
|
101
|
-
...issue,
|
|
102
|
-
path: [...ctx.path, ...issue.path], // proper path propagation
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
ctx.addIssue({
|
|
110
|
-
code: z.ZodIssueCode.custom,
|
|
111
|
-
message: `Value \`${val}\` of type "${typeof val}" should be a string or a nested object.`,
|
|
112
|
-
path: ctx.path,
|
|
113
|
-
});
|
|
114
|
-
})
|
|
115
|
-
)
|
|
116
|
-
)
|
|
117
|
-
.superRefine((obj, ctx) => {
|
|
118
|
-
for (const key of Object.keys(obj)) {
|
|
119
|
-
if (key.includes("$")) {
|
|
120
|
-
ctx.addIssue({
|
|
121
|
-
code: z.ZodIssueCode.custom,
|
|
122
|
-
message: `Key "${key}" should not include the "$" character.`,
|
|
123
|
-
path: [key],
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (key.includes("#")) {
|
|
127
|
-
ctx.addIssue({
|
|
128
|
-
code: z.ZodIssueCode.custom,
|
|
129
|
-
message: `Key "${key}" should not include the "#" character.`,
|
|
130
|
-
path: [key],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
if (!configKeyRegex.test(key)) {
|
|
134
|
-
ctx.addIssue({
|
|
135
|
-
code: z.ZodIssueCode.custom,
|
|
136
|
-
message: `Key "${key}" should only include whitespaces (s), lowercase letters (Ll), uppercase letters (Lu), other letters (Lo), numbers (N), dash punctuation (Pd), and connector punctuation (Pc).`,
|
|
137
|
-
path: [key],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const recursiveObjectResult = RecursiveObject.safeParse(config.data);
|
|
144
|
-
|
|
145
|
-
if (!recursiveObjectResult.success) {
|
|
146
|
-
console.warn("Config data could not pass validation from zod.");
|
|
147
|
-
recursiveObjectResult.error.errors.map((e) => console.log(e.message));
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ignores
|
|
152
|
-
|
|
153
|
-
const StringArraySchema = z.array(
|
|
154
|
-
z.string({
|
|
155
|
-
message: `The config's "ignores" key array should be made of string or be empty.`,
|
|
156
|
-
}),
|
|
157
|
-
{
|
|
158
|
-
message: `The config's "ignores" key value should be an array of strings (or at the very least an empty array).`,
|
|
159
|
-
}
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const stringArraySchemaResult = StringArraySchema.safeParse(config.ignores);
|
|
163
|
-
|
|
164
|
-
if (!stringArraySchemaResult.success) {
|
|
165
|
-
console.warn("Config ignores could not pass validation from zod.");
|
|
166
|
-
stringArraySchemaResult.error.errors.map((e) => console.log(e.message));
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Step 4: Do your thing
|
|
171
|
-
console.log("Running with config:", config);
|
|
172
|
-
return {
|
|
173
|
-
...flattenConfigData(config.data), // finalized
|
|
174
|
-
configPath, // finalized
|
|
175
|
-
passedIgnores: config.ignores, // could be treated here eventually
|
|
176
|
-
};
|
|
177
|
-
}
|