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.
@@ -1,4 +1,4 @@
1
- const config = {
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
- exitDueToFailure: "Terminates the whole process with a 'failure' code (1).",
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
- // export default config;
96
+ const ignores = ["chocolat.js"];
27
97
 
28
- const trueConfig = {
29
- data: config,
30
- ignores: ["chocolat.js"],
98
+ const config = {
99
+ data,
100
+ ignores,
31
101
  };
32
102
 
33
- export default trueConfig;
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}} reversedFlattenedConfig
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 = (reversedFlattenedConfig) => {
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 sortedReversedFlattenedConfig = Object.entries(
13
- reversedFlattenedConfig
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<string, []>} */
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 sortedReversedFlattenedConfig) {
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}} flattenedConfig
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 = (flattenedConfig) => {
12
- /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
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 = flattenedConfig[key];
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
- * Helper to process and recursively resolve a single import path.
11
- * Returns false if resolution fails at any level.
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
- visited,
23
+ visitedSet,
18
24
  depth,
19
25
  maxDepth
20
26
  ) => {
21
27
  const resolvedPath = resolveImportingPath(currentDir, importPath, cwd);
22
- if (!resolvedPath) return true; // Skips unresolved paths (not an error).
28
+ if (!resolvedPath) return true;
23
29
 
24
30
  const result = findAllImports(
25
31
  resolvedPath,
26
32
  cwd,
27
- visited,
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
- visited = new Set(),
52
+ visitedSet = new Set(),
38
53
  depth = 0,
39
54
  maxDepth = 100
40
55
  ) => {
41
- // Early failure checks (with logging).
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
- // Parses AST.
53
- visited.add(filePath);
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
- visited,
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
- visited,
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 visited; // success
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
- import makeResolveRule from "../rules/resolve.js";
15
- import makeCompressRule from "../rules/compress.js";
15
+ /* coreCommentsFlow */
16
16
 
17
- const coreCommentsFlow = async (
18
- ruleName,
19
- ignores,
20
- makeCommentsRule,
21
- flattenedConfigData
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]: makeCommentsRule(flattenedConfigData),
36
+ [ruleName]: ruleNames_makeRules[ruleName](flattenedConfigData),
36
37
  },
37
38
  },
38
39
  },
39
40
  rules: {
40
- [`${commentVariablesPluginName}/${ruleName}`]: "warn", // doesn't block builds, just applies fix
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} comment${total === 1 ? "" : "s"}.`
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
- resolveRuleName,
88
- ignores,
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 { runWithConfig } from "./_commons/utilities/run-with-config.js";
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 runWithConfig(rawConfigPath);
60
- if (!results) exitDueToFailure();
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.log(
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,6 +1,6 @@
1
1
  {
2
2
  "name": "comment-variables",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A CLI tool for configuring, managing and maintaining JavaScript comments as JavaScript variables.",
5
5
  "bin": {
6
6
  "jscomments": "./library/index.js",
@@ -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
- }