comment-variables 0.1.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 +88 -9
- package/library/_commons/constants/bases.js +77 -0
- package/library/_commons/constants/rules.js +9 -0
- package/library/_commons/rules/compress.js +78 -0
- package/library/_commons/rules/resolve.js +77 -0
- package/library/_commons/schemas/config.js +67 -0
- package/library/_commons/utilities/find-all-imports.js +120 -0
- package/library/_commons/utilities/flatten-config-data.js +107 -0
- package/library/_commons/utilities/flows.js +110 -0
- package/library/_commons/utilities/helpers.js +17 -0
- package/library/_commons/utilities/resolve-config.js +67 -0
- package/library/index.js +127 -0
- package/package.json +13 -8
- package/comments.config.ts +0 -24
- package/find-all-imports.js +0 -102
- package/import.js +0 -5
- package/import2.js +0 -1
- package/index.js +0 -384
- package/run-with-config.js +0 -138
- package/test-file.js +0 -7
package/comments.config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const config = {
|
|
1
|
+
const data = {
|
|
2
|
+
// for testing
|
|
4
3
|
levelOne: {
|
|
5
4
|
levelTwo: {
|
|
6
5
|
levelThree: "Level three.",
|
|
@@ -11,14 +10,94 @@ const config = {
|
|
|
11
10
|
levelThreeAlso: "Also level three here.",
|
|
12
11
|
levelThreeToo: "This too is level three.",
|
|
13
12
|
// test: "LEVELONE#LEVELTWO#LEVELTHREE", // errors
|
|
14
|
-
[`level$Three#First
|
|
15
|
-
|
|
16
|
-
with whitespaces. `,
|
|
17
|
-
testing: 2,
|
|
13
|
+
// [`level$Three#First
|
|
14
|
+
// whitespace`]: `This is level three
|
|
15
|
+
// with whitespaces. `, // fails
|
|
16
|
+
// testing: 2, // fails
|
|
17
|
+
// ".'e": "",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
|
+
// for deving
|
|
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
|
+
}),
|
|
20
94
|
};
|
|
21
95
|
|
|
22
|
-
|
|
96
|
+
const ignores = ["chocolat.js"];
|
|
23
97
|
|
|
24
|
-
|
|
98
|
+
const config = {
|
|
99
|
+
data,
|
|
100
|
+
ignores,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default config;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import tseslint from "typescript-eslint";
|
|
5
|
+
|
|
6
|
+
// plugin name
|
|
7
|
+
export const commentVariablesPluginName = "comment-variables";
|
|
8
|
+
|
|
9
|
+
// rule names
|
|
10
|
+
export const resolveRuleName = "resolve";
|
|
11
|
+
export const compressRuleName = "compress";
|
|
12
|
+
|
|
13
|
+
// current working directory
|
|
14
|
+
export const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
// to prevent accidental changes
|
|
17
|
+
export const hasPackageJson = fs.existsSync(path.join(cwd, "package.json"));
|
|
18
|
+
// to prevent irreversible changes
|
|
19
|
+
export const hasGitFolder = fs.existsSync(path.join(cwd, ".git"));
|
|
20
|
+
|
|
21
|
+
// comments.config.js
|
|
22
|
+
export const defaultConfigFileName = "comments.config.js";
|
|
23
|
+
|
|
24
|
+
// flags
|
|
25
|
+
export const configFlag = "--config";
|
|
26
|
+
export const lintConfigImportsFlag = "--lint-config-imports";
|
|
27
|
+
export const myIgnoresOnlyFlag = "--my-ignores-only";
|
|
28
|
+
|
|
29
|
+
// ESLint ignores
|
|
30
|
+
export const knownIgnores = [
|
|
31
|
+
".next",
|
|
32
|
+
".react-router",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".parcel-cache",
|
|
35
|
+
".react-router-parcel",
|
|
36
|
+
"dist",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// ESLint file globs
|
|
40
|
+
export const allJSTSFileGlobs = [
|
|
41
|
+
"**/*.js",
|
|
42
|
+
"**/*.jsx",
|
|
43
|
+
"**/*.ts",
|
|
44
|
+
"**/*.tsx",
|
|
45
|
+
"**/*.mjs",
|
|
46
|
+
"**/*.cjs",
|
|
47
|
+
];
|
|
48
|
+
export const allMDFileGlobs = ["**/*.md"];
|
|
49
|
+
export const allMDVirtualJSTSFileGlobs = [
|
|
50
|
+
"**/*.md/*.js",
|
|
51
|
+
"**/*.md/*.jsx",
|
|
52
|
+
"**/*.md/*.ts",
|
|
53
|
+
"**/*.md/*.tsx",
|
|
54
|
+
"**/*.md/*.cjs",
|
|
55
|
+
"**/*.md/*.mjs",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// default ESLint config language options
|
|
59
|
+
export const typeScriptAndJSXCompatible = {
|
|
60
|
+
// for compatibility with TypeScript (.ts and .tsx)
|
|
61
|
+
parser: tseslint.parser,
|
|
62
|
+
// for compatibility with JSX (React, etc.)
|
|
63
|
+
parserOptions: {
|
|
64
|
+
ecmaFeatures: {
|
|
65
|
+
jsx: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// messageId
|
|
71
|
+
export const placeholderMessageId = "placeholderMessageId";
|
|
72
|
+
|
|
73
|
+
// regexes
|
|
74
|
+
export const configKeyRegex = /^[\p{Ll}\p{Lu}\p{Lo}\p{Pd}\p{Pc}\p{N}\s]+$/u;
|
|
75
|
+
export const flattenedConfigKeyRegex = /^[\p{Lu}\p{Lo}\p{Pd}\p{Pc}\p{N}#]+$/u; // same as configKeyRegex but without lowercase letters (\p{Ll}), without whitespaces (\s which are replaced by underscores) and with the '#' character (that links each subkey together)
|
|
76
|
+
export const flattenedConfigPlaceholderRegex =
|
|
77
|
+
/\$COMMENT#([\p{Lu}\p{Lo}\p{Pd}\p{Pc}\p{N}_#]+)/gu; // same as flattenedConfigKeyRegex but taking the prefix $COMMENT# into consideration, removing ^ and $ in the capture group, globally
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { placeholderMessageId } from "../constants/bases.js";
|
|
2
|
+
|
|
3
|
+
import { escapeRegex } from "..//utilities/helpers.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
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
|
+
*/
|
|
10
|
+
const makeRule = (reversedFlattenedConfigData) => {
|
|
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 sortedReversedFlattenedConfigData = Object.entries(
|
|
13
|
+
reversedFlattenedConfigData
|
|
14
|
+
).sort(([a], [b]) => b.length - a.length);
|
|
15
|
+
|
|
16
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<typeof placeholderMessageId, []>} */
|
|
17
|
+
const rule = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "suggestion",
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
"Compresses comments into $COMMENT#* placeholder(s) in comment line(s) or block(s).",
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
[placeholderMessageId]:
|
|
26
|
+
"Compressed comments into $COMMENT#* placeholder(s) in comment line(s) or block(s).",
|
|
27
|
+
},
|
|
28
|
+
fixable: "code",
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
create: (context) => {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
const comments = sourceCode
|
|
34
|
+
.getAllComments()
|
|
35
|
+
.filter((e) => e.type !== "Shebang");
|
|
36
|
+
|
|
37
|
+
for (const comment of comments) {
|
|
38
|
+
let fixedText = comment.value;
|
|
39
|
+
let modified = false;
|
|
40
|
+
|
|
41
|
+
for (const [
|
|
42
|
+
resolvedValue,
|
|
43
|
+
commentKey,
|
|
44
|
+
] of sortedReversedFlattenedConfigData) {
|
|
45
|
+
const pattern = new RegExp(
|
|
46
|
+
`(?<=\\s|^)${escapeRegex(resolvedValue)}(?=\\s|$)`,
|
|
47
|
+
"g"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
fixedText = fixedText.replace(pattern, () => {
|
|
51
|
+
modified = true;
|
|
52
|
+
return `$COMMENT#${commentKey}`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (modified && fixedText !== comment.value) {
|
|
57
|
+
context.report({
|
|
58
|
+
loc: comment.loc,
|
|
59
|
+
messageId: placeholderMessageId,
|
|
60
|
+
fix(fixer) {
|
|
61
|
+
const fullCommentText =
|
|
62
|
+
comment.type === "Block"
|
|
63
|
+
? `/*${fixedText}*/`
|
|
64
|
+
: `//${fixedText}`;
|
|
65
|
+
return fixer.replaceText(comment, fullCommentText);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return rule;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default makeRule; // compress
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
placeholderMessageId,
|
|
3
|
+
flattenedConfigPlaceholderRegex,
|
|
4
|
+
} from "../constants/bases.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
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
|
+
*/
|
|
11
|
+
const makeRule = (flattenedConfigData) => {
|
|
12
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<typeof placeholderMessageId, []>} */
|
|
13
|
+
const rule = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: "suggestion",
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
"Resolves $COMMENT#* placeholder(s) in comment line(s) or block(s).",
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
[placeholderMessageId]:
|
|
22
|
+
"Resolved $COMMENT#* placeholder(s) in comment line(s) or block(s).",
|
|
23
|
+
},
|
|
24
|
+
fixable: "code",
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
create: (context) => {
|
|
28
|
+
const sourceCode = context.sourceCode;
|
|
29
|
+
const comments = sourceCode
|
|
30
|
+
.getAllComments()
|
|
31
|
+
.filter((e) => e.type !== "Shebang");
|
|
32
|
+
|
|
33
|
+
for (const comment of comments) {
|
|
34
|
+
const matches = [
|
|
35
|
+
...comment.value.matchAll(flattenedConfigPlaceholderRegex),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (matches.length === 0) continue;
|
|
39
|
+
|
|
40
|
+
let fixedText = comment.value;
|
|
41
|
+
let hasValidFix = false;
|
|
42
|
+
|
|
43
|
+
for (const match of matches) {
|
|
44
|
+
const fullMatch = match[0]; // e.g. $COMMENT#LEVELONE#LEVELTWO
|
|
45
|
+
const key = match[1]; // e.g. LEVELONE#LEVELTWO
|
|
46
|
+
const replacement = flattenedConfigData[key];
|
|
47
|
+
|
|
48
|
+
if (replacement) {
|
|
49
|
+
fixedText = fixedText.replace(fullMatch, replacement);
|
|
50
|
+
hasValidFix = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (hasValidFix && fixedText !== comment.value) {
|
|
55
|
+
context.report({
|
|
56
|
+
loc: comment.loc,
|
|
57
|
+
messageId: placeholderMessageId,
|
|
58
|
+
fix(fixer) {
|
|
59
|
+
const range = comment.range;
|
|
60
|
+
const prefix = comment.type === "Block" ? "/*" : "//";
|
|
61
|
+
const suffix = comment.type === "Block" ? "*/" : "";
|
|
62
|
+
const newComment = `${prefix}${fixedText}${suffix}`;
|
|
63
|
+
|
|
64
|
+
return fixer.replaceTextRange(range, newComment);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return rule;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default makeRule; // resolve
|
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { resolveImportingPath } from "resolve-importing-path";
|
|
5
|
+
import { getSourceCodeFromFilePath } from "get-sourcecode-from-file-path";
|
|
6
|
+
|
|
7
|
+
/* findAllImports */
|
|
8
|
+
|
|
9
|
+
/**
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
const processImport = (
|
|
20
|
+
importPath,
|
|
21
|
+
currentDir,
|
|
22
|
+
cwd,
|
|
23
|
+
visitedSet,
|
|
24
|
+
depth,
|
|
25
|
+
maxDepth
|
|
26
|
+
) => {
|
|
27
|
+
const resolvedPath = resolveImportingPath(currentDir, importPath, cwd);
|
|
28
|
+
if (!resolvedPath) return true;
|
|
29
|
+
|
|
30
|
+
const result = findAllImports(
|
|
31
|
+
resolvedPath,
|
|
32
|
+
cwd,
|
|
33
|
+
visitedSet,
|
|
34
|
+
depth + 1,
|
|
35
|
+
maxDepth
|
|
36
|
+
);
|
|
37
|
+
return result !== null; // Returns false if child failed.
|
|
38
|
+
};
|
|
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
|
+
*/
|
|
49
|
+
export const findAllImports = (
|
|
50
|
+
filePath,
|
|
51
|
+
cwd = process.cwd(),
|
|
52
|
+
visitedSet = new Set(),
|
|
53
|
+
depth = 0,
|
|
54
|
+
maxDepth = 100
|
|
55
|
+
) => {
|
|
56
|
+
// Fails early if max depth is recursively reached.
|
|
57
|
+
if (depth > maxDepth) {
|
|
58
|
+
console.error(`ERROR. Max depth ${maxDepth} reached at ${filePath}.`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Fails early if no file is found.
|
|
62
|
+
if (!fs.existsSync(filePath)) {
|
|
63
|
+
console.error(`ERROR. File not found at ${filePath}.`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
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.
|
|
72
|
+
const sourceCode = getSourceCodeFromFilePath(filePath);
|
|
73
|
+
if (!sourceCode?.ast) {
|
|
74
|
+
console.error(`ERROR. Failed to parse AST for ${filePath}.`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Processes all imports.
|
|
79
|
+
const currentDir = path.dirname(filePath);
|
|
80
|
+
for (const node of sourceCode.ast.body) {
|
|
81
|
+
// ES Modules (import x from 'y')
|
|
82
|
+
if (node.type === "ImportDeclaration") {
|
|
83
|
+
if (
|
|
84
|
+
!processImport(
|
|
85
|
+
node.source.value,
|
|
86
|
+
currentDir,
|
|
87
|
+
cwd,
|
|
88
|
+
visitedSet,
|
|
89
|
+
depth,
|
|
90
|
+
maxDepth
|
|
91
|
+
)
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CommonJS (require('x'))
|
|
98
|
+
if (
|
|
99
|
+
node.type === "ExpressionStatement" &&
|
|
100
|
+
node.expression.type === "CallExpression" &&
|
|
101
|
+
node.expression.callee.name === "require" &&
|
|
102
|
+
node.expression.arguments[0]?.type === "Literal"
|
|
103
|
+
) {
|
|
104
|
+
if (
|
|
105
|
+
!processImport(
|
|
106
|
+
node.expression.arguments[0].value,
|
|
107
|
+
currentDir,
|
|
108
|
+
cwd,
|
|
109
|
+
visitedSet,
|
|
110
|
+
depth,
|
|
111
|
+
maxDepth
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return visitedSet; // success
|
|
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
|
+
};
|