comment-variables 0.0.5 → 0.2.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,6 +1,5 @@
1
- import { test } from "./import.js";
2
-
3
1
  const config = {
2
+ // for testing
4
3
  levelOne: {
5
4
  levelTwo: {
6
5
  levelThree: "Level three.",
@@ -11,14 +10,24 @@ 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
- whitespace`]: `This is level three
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: {
22
+ exitDueToFailure: "Terminates the whole process with a 'failure' code (1).",
23
+ },
20
24
  };
21
25
 
22
- export default config;
26
+ // export default config;
27
+
28
+ const trueConfig = {
29
+ data: config,
30
+ ignores: ["chocolat.js"],
31
+ };
23
32
 
24
- // This too is level three.
33
+ export default trueConfig;
@@ -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,78 @@
1
+ import { placeholderMessageId } from "../constants/bases.js";
2
+
3
+ import { escapeRegex } from "..//utilities/helpers.js";
4
+
5
+ /**
6
+ *
7
+ * @param {{[key: string]: string}} reversedFlattenedConfig
8
+ * @returns
9
+ */
10
+ const makeRule = (reversedFlattenedConfig) => {
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
14
+ ).sort(([a], [b]) => b.length - a.length);
15
+
16
+ /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
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 sortedReversedFlattenedConfig) {
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
+ *
8
+ * @param {{[key: string]: string}} flattenedConfig
9
+ * @returns
10
+ */
11
+ const makeRule = (flattenedConfig) => {
12
+ /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
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 = flattenedConfig[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,102 @@
1
+ import { ESLint } from "eslint";
2
+ import markdown from "@eslint/markdown";
3
+
4
+ import {
5
+ commentVariablesPluginName,
6
+ resolveRuleName,
7
+ compressRuleName,
8
+ allJSTSFileGlobs,
9
+ allMDFileGlobs,
10
+ allMDVirtualJSTSFileGlobs,
11
+ typeScriptAndJSXCompatible,
12
+ } from "../constants/bases.js";
13
+
14
+ import makeResolveRule from "../rules/resolve.js";
15
+ import makeCompressRule from "../rules/compress.js";
16
+
17
+ const coreCommentsFlow = async (
18
+ ruleName,
19
+ ignores,
20
+ makeCommentsRule,
21
+ flattenedConfigData
22
+ ) => {
23
+ const eslint = new ESLint({
24
+ fix: true,
25
+ errorOnUnmatchedPattern: false,
26
+ overrideConfigFile: true,
27
+ overrideConfig: [
28
+ {
29
+ files: allJSTSFileGlobs,
30
+ ignores,
31
+ languageOptions: typeScriptAndJSXCompatible,
32
+ plugins: {
33
+ [commentVariablesPluginName]: {
34
+ rules: {
35
+ [ruleName]: makeCommentsRule(flattenedConfigData),
36
+ },
37
+ },
38
+ },
39
+ rules: {
40
+ [`${commentVariablesPluginName}/${ruleName}`]: "warn", // doesn't block builds, just applies fix
41
+ },
42
+ },
43
+ {
44
+ files: allMDFileGlobs,
45
+ ignores,
46
+ plugins: { markdown },
47
+ processor: "markdown/markdown",
48
+ },
49
+ {
50
+ files: allMDVirtualJSTSFileGlobs,
51
+ ignores,
52
+ languageOptions: typeScriptAndJSXCompatible,
53
+ rules: {
54
+ [`${commentVariablesPluginName}/${ruleName}`]: "warn",
55
+ },
56
+ },
57
+ ],
58
+ });
59
+
60
+ const results = await eslint.lintFiles([
61
+ ...allJSTSFileGlobs,
62
+ ...allMDFileGlobs,
63
+ ]);
64
+ await ESLint.outputFixes(results);
65
+
66
+ console.log({ results });
67
+
68
+ const resolvedOrCompressed =
69
+ ruleName === resolveRuleName
70
+ ? "Resolved"
71
+ : ruleName === compressRuleName
72
+ ? "Compressed"
73
+ : "Unknown rule name'd";
74
+
75
+ const total = results.reduce((sum, r) => {
76
+ const add = r.output ? 1 : 0;
77
+ return sum + add;
78
+ }, 0);
79
+
80
+ console.log(
81
+ `✅ ${resolvedOrCompressed} ${total} comment${total === 1 ? "" : "s"}.`
82
+ );
83
+ };
84
+
85
+ export const resolveCommentsFlow = async (ignores, flattenedConfigData) =>
86
+ coreCommentsFlow(
87
+ resolveRuleName,
88
+ ignores,
89
+ makeResolveRule,
90
+ flattenedConfigData
91
+ );
92
+
93
+ export const compressCommentsFlow = async (
94
+ ignores,
95
+ reversedFlattenedConfigData
96
+ ) =>
97
+ coreCommentsFlow(
98
+ compressRuleName,
99
+ ignores,
100
+ makeCompressRule,
101
+ reversedFlattenedConfigData
102
+ );
@@ -0,0 +1,15 @@
1
+ /* exitDueToFailure */
2
+
3
+ /**
4
+ * Terminates the whole process with a 'failure' code (1).
5
+ * @returns Never.
6
+ */
7
+ export const exitDueToFailure = () => process.exit(1);
8
+
9
+ /**
10
+ * Escapes all regex characters with a `"\"` in a string to prepare it for use in a regex
11
+ * @param {string} string The string.
12
+ * @returns The string with regex characters escaped.
13
+ */
14
+ export const escapeRegex = (string) =>
15
+ string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3,14 +3,16 @@ import { pathToFileURL } from "url";
3
3
 
4
4
  import { z } from "zod";
5
5
 
6
- function flattenConfig(
7
- config,
6
+ import { configKeyRegex } from "../constants/bases.js";
7
+
8
+ function flattenConfigData(
9
+ configData,
8
10
  normalizedPath = "",
9
11
  map = {},
10
12
  pathStack = [],
11
- reversedFlattenedConfig = {}
13
+ reversedFlattenedConfigData = {}
12
14
  ) {
13
- for (const [key, value] of Object.entries(config)) {
15
+ for (const [key, value] of Object.entries(configData)) {
14
16
  const currentPath = [...pathStack, key];
15
17
  normalizedPath = currentPath
16
18
  .map((k) => k.toUpperCase())
@@ -31,18 +33,18 @@ function flattenConfig(
31
33
  __source: currentPath.join(" > "), // for debugging
32
34
  };
33
35
  } else if (typeof value === "object") {
34
- flattenConfig(value, normalizedPath, map, currentPath);
36
+ flattenConfigData(value, normalizedPath, map, currentPath);
35
37
  }
36
38
  }
37
39
 
38
- const flattenedConfig = Object.fromEntries(
40
+ const flattenedConfigData = Object.fromEntries(
39
41
  Object.entries(map).map(([k, v]) => [k, v.value])
40
42
  ); // strip metadata
41
43
 
42
44
  const set = new Set();
43
45
 
44
46
  // the integrity of the config needs to be established before working with it
45
- for (const value of Object.values(flattenedConfig)) {
47
+ for (const value of Object.values(flattenedConfigData)) {
46
48
  if (set.has(value)) {
47
49
  // checks that no two values are duplicate
48
50
  throw new Error(
@@ -52,11 +54,14 @@ function flattenConfig(
52
54
  set.add(value);
53
55
  }
54
56
 
55
- for (const [key, value] of Object.entries(flattenedConfig)) {
56
- reversedFlattenedConfig[value] = key;
57
+ for (const [key, value] of Object.entries(flattenedConfigData)) {
58
+ reversedFlattenedConfigData[value] = key;
57
59
  }
58
60
 
59
- return { flattenedConfig, reversedFlattenedConfig };
61
+ return {
62
+ flattenedConfigData,
63
+ reversedFlattenedConfigData,
64
+ };
60
65
  }
61
66
 
62
67
  export async function runWithConfig(configPath) {
@@ -71,11 +76,15 @@ export async function runWithConfig(configPath) {
71
76
  const config = configModule.default;
72
77
 
73
78
  // Step 3: Validate config object
74
- if (!config || typeof config !== "object") {
75
- console.warn("Invalid config format. Exiting.");
79
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
80
+ console.warn(
81
+ "Invalid config format. The config should be an object. Exiting."
82
+ );
76
83
  return null;
77
84
  }
78
85
 
86
+ // data
87
+
79
88
  const RecursiveObject = z
80
89
  .lazy(() =>
81
90
  z.record(
@@ -121,18 +130,48 @@ export async function runWithConfig(configPath) {
121
130
  path: [key],
122
131
  });
123
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
+ }
124
140
  }
125
141
  });
126
142
 
127
- const result = RecursiveObject.safeParse(config);
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);
128
163
 
129
- if (!result.success) {
130
- console.warn("Config could not pass validation from zod.");
131
- result.error.errors.map((e) => console.log(e.message));
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));
132
167
  return null;
133
168
  }
134
169
 
135
170
  // Step 4: Do your thing
136
171
  console.log("Running with config:", config);
137
- return { ...flattenConfig(config), configPath };
172
+ return {
173
+ ...flattenConfigData(config.data), // finalized
174
+ configPath, // finalized
175
+ passedIgnores: config.ignores, // could be treated here eventually
176
+ };
138
177
  }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // The shebang (#!) is necessary to communicate with Unix-based systems, like Linux and macOS. On Windows, it is ignored, but npm tooling bridges the gap by generating wrappers that make the CLI work anyway.
3
+
4
+ import path from "path";
5
+
6
+ import {
7
+ cwd,
8
+ hasPackageJson,
9
+ hasGitFolder,
10
+ defaultConfigFileName,
11
+ configFlag,
12
+ lintConfigImportsFlag,
13
+ myIgnoresOnlyFlag,
14
+ knownIgnores,
15
+ } from "./_commons/constants/bases.js";
16
+
17
+ import { exitDueToFailure } from "./_commons/utilities/helpers.js";
18
+ import { runWithConfig } from "./_commons/utilities/run-with-config.js";
19
+ import { findAllImports } from "./_commons/utilities/find-all-imports.js";
20
+
21
+ import {
22
+ resolveCommentsFlow,
23
+ compressCommentsFlow,
24
+ } from "./_commons/utilities/flows.js";
25
+
26
+ // ENSURES THE CLI TOOL ONLY RUN IN FOLDERS THAT POSSESS A package.json FILE AND A .git FOLDER.
27
+
28
+ if (!hasPackageJson) {
29
+ console.error(
30
+ "ERROR. No package.json file found in this directory. Aborting to prevent accidental changes."
31
+ );
32
+ exitDueToFailure();
33
+ }
34
+ if (!hasGitFolder) {
35
+ console.error(
36
+ "ERROR. No git folder found in this directory. Aborting to prevent irreversible changes."
37
+ );
38
+ exitDueToFailure();
39
+ }
40
+
41
+ // GATHERS COMMANDS.
42
+
43
+ const commands = process.argv;
44
+
45
+ // OBTAINS THE VALIDATED FLATTENED CONFIG, REVERSE FLATTENED CONFIG, AND CONFIG PATH.
46
+
47
+ // extracts the position of the --config flag
48
+ const configFlagIndex = commands.indexOf(configFlag);
49
+ // determines if there's a valid config flag input
50
+ const hasConfigFlag = configFlagIndex >= 2;
51
+ // determines if there's an actual config path passed to the config flag
52
+ const passedConfig = commands[configFlagIndex + 1];
53
+ // gets the absolute passed config path if the --config flag is set
54
+ const passedConfigPath =
55
+ hasConfigFlag && passedConfig ? path.join(cwd, passedConfig) : null;
56
+ // defaults to comments.config.js if no --config flag is set
57
+ const rawConfigPath = passedConfigPath ?? path.join(cwd, defaultConfigFileName);
58
+
59
+ const results = await runWithConfig(rawConfigPath);
60
+ if (!results) exitDueToFailure();
61
+
62
+ const {
63
+ flattenedConfigData,
64
+ reversedFlattenedConfigData,
65
+ configPath,
66
+ passedIgnores,
67
+ } = results;
68
+
69
+ console.log("Flattened config is:", flattenedConfigData);
70
+ console.log("Reversed flattened config is:", reversedFlattenedConfigData);
71
+ console.log("Config path is:", configPath);
72
+ console.log("Passed ignores are:", passedIgnores);
73
+
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
+ // ADDRESSES THE --lint-config-imports FLAG, GIVEN THAT THE FILES IMPORTED BY THE CONFIG ARE IGNORED BY DEFAULT.
89
+
90
+ const lintConfigImports = commands.indexOf(lintConfigImportsFlag) >= 2;
91
+ const rawConfigPathIgnores = lintConfigImports
92
+ ? [configPath]
93
+ : [...findAllImports(configPath)];
94
+
95
+ // the ignore paths must be relative
96
+ const configPathIgnores = rawConfigPathIgnores.map((e) =>
97
+ path.relative(cwd, e)
98
+ );
99
+
100
+ console.log(
101
+ lintConfigImports ? "Config path ignore is:" : "Config path ignores are:",
102
+ configPathIgnores
103
+ );
104
+
105
+ // ADDRESSES THE --my-ignores-only FLAG, GIVEN THAT KNOWN IGNORES ARE IGNORED BY DEFAULT
106
+
107
+ const myIgnoresOnly = commands.indexOf(myIgnoresOnlyFlag) >= 2;
108
+ const rawIgnores = [...configPathIgnores, ...passedIgnores];
109
+ const ignores = myIgnoresOnly ? rawIgnores : [...rawIgnores, ...knownIgnores];
110
+
111
+ console.log("Ignores are:", ignores);
112
+
113
+ // ADDRESSES THE CORE COMMANDS "resolve" AND "compress".
114
+
115
+ const coreCommand = commands[2];
116
+
117
+ switch (coreCommand) {
118
+ case "resolve":
119
+ await resolveCommentsFlow(ignores, flattenedConfigData);
120
+ break;
121
+ case "compress":
122
+ await compressCommentsFlow(ignores, reversedFlattenedConfigData);
123
+ break;
124
+ case undefined: // falls through the default
125
+ default:
126
+ if (coreCommand && !coreCommand.startsWith("--"))
127
+ console.log(
128
+ `Core command not recognized. Choose between "resolve" and "compress".`
129
+ );
130
+ else
131
+ console.log(
132
+ `If these settings are correct with you, feel free to initiate the command "resolve" to resolve comments, or "compress" to compress them back to their $COMMENT#* forms.${
133
+ passedConfigPath || lintConfigImports || myIgnoresOnly
134
+ ? " (And DON'T FORGET YOUR FLAGS!)"
135
+ : ""
136
+ }`
137
+ );
138
+ break;
139
+ }
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,12 +1,12 @@
1
1
  {
2
2
  "name": "comment-variables",
3
- "version": "0.0.5",
4
- "description": "",
3
+ "version": "0.2.0",
4
+ "description": "A CLI tool for configuring, managing and maintaining JavaScript comments as JavaScript variables.",
5
5
  "bin": {
6
- "jscomments": "./index.js",
7
- "comment-variables": "./index.js"
6
+ "jscomments": "./library/index.js",
7
+ "comment-variables": "./library/index.js"
8
8
  },
9
- "main": "index.js",
9
+ "main": "library/index.js",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/LutherTS/jscomments.git"
@@ -14,13 +14,19 @@
14
14
  "scripts": {
15
15
  "test": "echo \"Error: no test specified\" && exit 1"
16
16
  },
17
- "keywords": [],
18
- "author": "",
17
+ "keywords": [
18
+ "cli",
19
+ "javascript",
20
+ "comments",
21
+ "variables"
22
+ ],
23
+ "author": "Luther Tchofo Safo <luther@tchofo-safo-portfolio.me>",
19
24
  "license": "MIT",
20
25
  "type": "module",
21
26
  "dependencies": {
27
+ "@eslint/markdown": "^6.5.0",
22
28
  "eslint": "^9.29.0",
23
- "get-sourcecode-from-file-path": "^1.0.0",
29
+ "get-sourcecode-from-file-path": "^1.0.1",
24
30
  "resolve-importing-path": "^1.0.2",
25
31
  "tsconfig-paths": "^4.2.0",
26
32
  "typescript-eslint": "^8.34.1",
@@ -1,24 +0,0 @@
1
- import { test } from "./import.js";
2
-
3
- const config = {
4
- levelOne: {
5
- levelTwo: {
6
- levelThree: "Level three.",
7
- // levelthree: "Also level three.", // errors
8
- // alsoLevelThree: "Level three.", // errors
9
- levelThreeBis: "Level three bis.",
10
- levelThreeTer: "Level three ter.",
11
- levelThreeAlso: "Also level three here.",
12
- levelThreeToo: "This too is level three.",
13
- // test: "LEVELONE#LEVELTWO#LEVELTHREE", // errors
14
- },
15
- },
16
- };
17
-
18
- export default config;
19
-
20
- // This too is level three.
21
-
22
- /* Notes
23
- I'll need to install TypeScript to test this.
24
- */
package/import.js DELETED
@@ -1,5 +0,0 @@
1
- import { test2 } from "./import2.js";
2
-
3
- export const test = test2;
4
-
5
- // This too is level three.
package/import2.js DELETED
@@ -1 +0,0 @@
1
- export const test2 = "test2";
package/index.js DELETED
@@ -1,343 +0,0 @@
1
- #!/usr/bin/env node
2
- // The hashbang (#!) is necessary to communicate with Unix-based systems, like Linux and macOS. On Windows, it is ignored, but npm tooling bridges the gap by generating wrappers that make the CLI work anyway.
3
-
4
- import path from "path";
5
- import fs from "fs";
6
-
7
- import { ESLint } from "eslint";
8
- import tseslint from "typescript-eslint";
9
-
10
- import { runWithConfig } from "./run-with-config.js";
11
- import { findAllImports } from "./find-all-imports.js";
12
-
13
- const cwd = process.cwd();
14
-
15
- // ENSURES THE CLI TOOL ONLY RUN IN FOLDERS THAT POSSESS A package.json FILE AND A .git FOLDER.
16
-
17
- const hasPackageJson = fs.existsSync(path.join(cwd, "package.json"));
18
- if (!hasPackageJson) {
19
- console.error(
20
- "ERROR. No package.json file found in this directory. Aborting to prevent accidental changes."
21
- );
22
- process.exit(1);
23
- }
24
- const hasGitFolder = fs.existsSync(path.join(cwd, ".git"));
25
- if (!hasGitFolder) {
26
- console.error(
27
- "ERROR. No git folder found in this directory. Aborting to prevent irreversible changes."
28
- );
29
- process.exit(1);
30
- }
31
-
32
- // GATHERS COMMANDS.
33
-
34
- const commands = process.argv;
35
-
36
- // OBTAINS THE VALIDATED FLATTENED CONFIG, REVERSE FLATTENED CONFIG, AND CONFIG PATH.
37
-
38
- const configFlagIndex = commands.indexOf("--config");
39
- const passedConfigPath =
40
- configFlagIndex >= 2 ? path.join(cwd, commands[configFlagIndex + 1]) : null;
41
- const rawConfigPath = passedConfigPath ?? path.join(cwd, "comments.config.js");
42
-
43
- const results = await runWithConfig(rawConfigPath);
44
- if (!results) process.exit(1);
45
-
46
- const { flattenedConfig, reversedFlattenedConfig, configPath } = results;
47
- console.log("Config path is:", configPath);
48
- console.log("Verified flattened config is:", flattenedConfig);
49
- console.log("Reversed flattened config is:", reversedFlattenedConfig);
50
-
51
- const keys = new Set([...Object.keys(flattenedConfig)]);
52
- const values = new Set([...Object.values(flattenedConfig)]);
53
-
54
- // VALIDATES ONE LAST TIME THE REVERSABILITY OF flattenedConfig AND reversedFlattenedConfig.
55
-
56
- keys.forEach((key) => {
57
- if (values.has(key)) {
58
- console.error(
59
- `The key "${key}" is and shouldn't be among the values of flattenedConfig.`
60
- );
61
- process.exit(1);
62
- }
63
- });
64
-
65
- // ADDRESSES THE --include-config-imports FLAG, GIVEN THAT THE FILES IMPORTED BY THE CONFIG ARE IGNORED BY DEFAULT.
66
-
67
- const includeConfigImports = commands.indexOf("--include-config-imports") >= 2;
68
- const rawConfigIgnores = includeConfigImports
69
- ? [configPath]
70
- : [...findAllImports(configPath)];
71
-
72
- // the ignore paths must be relative
73
- const configIgnores = rawConfigIgnores.map((e) => path.relative(cwd, e));
74
- console.log(
75
- includeConfigImports ? "Config ignore is:" : "Config ignores are",
76
- configIgnores
77
- );
78
-
79
- // DEFINES DEFAULT ESLINT IGNORES AND FILES.
80
-
81
- const knownIgnores = [
82
- ".next",
83
- ".react-router",
84
- "node_modules",
85
- ".parcel-cache",
86
- ".react-router-parcel",
87
- "dist",
88
- ];
89
-
90
- const allJSTSFileGlobs = [
91
- "**/*.tsx",
92
- "**/*.ts",
93
- "**/*.jsx",
94
- "**/*.js",
95
- "**/*.mjs",
96
- "**/*.cjs",
97
- ];
98
-
99
- // MAKES THE FLOW FOR resolveCommentsInProject.
100
-
101
- const typeScriptAndJSXCompatible = {
102
- // for compatibility with TypeScript (.ts and .tsx)
103
- parser: tseslint.parser,
104
- // for compatibility with JSX (React, etc.)
105
- parserOptions: {
106
- ecmaFeatures: {
107
- jsx: true,
108
- },
109
- },
110
- };
111
-
112
- /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
113
- const jsCommentsRule = {
114
- meta: {
115
- type: "suggestion",
116
- docs: {
117
- description: "Resolve $COMMENT#... using js-comments config.",
118
- },
119
- messages: {
120
- message: `Resolved $COMMENT placeholder(s) in comment.`,
121
- },
122
- fixable: "code",
123
- schema: [],
124
- },
125
- create: (context) => {
126
- const sourceCode = context.sourceCode;
127
- const comments = sourceCode
128
- .getAllComments()
129
- .filter((e) => e.type !== "Shebang");
130
-
131
- for (const comment of comments) {
132
- const matches = [...comment.value.matchAll(/\$COMMENT#([A-Z0-9#_]+)/g)];
133
-
134
- if (matches.length === 0) continue;
135
-
136
- let fixedText = comment.value;
137
- let hasValidFix = false;
138
-
139
- for (const match of matches) {
140
- const fullMatch = match[0]; // e.g. $COMMENT#LEVELONE#LEVELTWO
141
- const key = match[1]; // e.g. LEVELONE#LEVELTWO
142
- const replacement = flattenedConfig[key];
143
-
144
- if (replacement) {
145
- fixedText = fixedText.replace(fullMatch, replacement);
146
- hasValidFix = true;
147
- }
148
- }
149
-
150
- if (hasValidFix && fixedText !== comment.value) {
151
- context.report({
152
- loc: comment.loc,
153
- messageId: "message",
154
- fix(fixer) {
155
- const range = comment.range;
156
- const prefix = comment.type === "Block" ? "/*" : "//";
157
- const suffix = comment.type === "Block" ? "*/" : "";
158
- const newComment = `${prefix}${fixedText}${suffix}`;
159
-
160
- return fixer.replaceTextRange(range, newComment);
161
- },
162
- });
163
- }
164
- }
165
-
166
- return {};
167
- },
168
- };
169
-
170
- async function resolveCommentsInProject(fileGlobs = allJSTSFileGlobs) {
171
- const ruleName = "js-comments/js-comments-autofix";
172
-
173
- const eslint = new ESLint({
174
- fix: true,
175
- errorOnUnmatchedPattern: false,
176
- overrideConfigFile: true,
177
- overrideConfig: [
178
- {
179
- files: fileGlobs,
180
- ignores: [...configIgnores, ...knownIgnores], // 🚫 Ensure config isn't linted
181
- languageOptions: typeScriptAndJSXCompatible,
182
- plugins: {
183
- "js-comments": {
184
- rules: {
185
- "js-comments-autofix": jsCommentsRule,
186
- },
187
- },
188
- },
189
- rules: {
190
- [ruleName]: "warn", // Don't block builds, just apply fix
191
- },
192
- },
193
- ],
194
- });
195
-
196
- const results = await eslint.lintFiles(fileGlobs);
197
- await ESLint.outputFixes(results);
198
-
199
- console.log({ results });
200
-
201
- const total = results.reduce((sum, r) => {
202
- const add = r.output ? 1 : 0;
203
- return sum + add;
204
- }, 0);
205
- console.log(`✅ Resolved ${total} comment${total === 1 ? "" : "s"}.`);
206
- }
207
-
208
- // MAKES THE FLOW FOR compressCommentsInProject.
209
-
210
- const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
211
-
212
- const makeReverseJsCommentsRule = (reversedFlattenedConfig) => {
213
- // Sort the resolved values by descending length to prevent partial replacements.
214
- const sortedReversedFlattenedConfig = Object.entries(
215
- reversedFlattenedConfig
216
- ).sort(([a], [b]) => b.length - a.length);
217
-
218
- /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
219
- const reverseJsCommentsRule = {
220
- meta: {
221
- type: "suggestion",
222
- docs: {
223
- description: "Resolve $COMMENT#... using js-comments config in reverse",
224
- },
225
- messages: {
226
- message: `Comment compressed.`,
227
- },
228
- fixable: "code",
229
- schema: [],
230
- },
231
- create(context) {
232
- const sourceCode = context.sourceCode;
233
- const comments = sourceCode
234
- .getAllComments()
235
- .filter((e) => e.type !== "Shebang");
236
-
237
- for (const comment of comments) {
238
- let fixedText = comment.value;
239
- let modified = false;
240
-
241
- for (const [
242
- resolvedValue,
243
- commentKey,
244
- ] of sortedReversedFlattenedConfig) {
245
- const pattern = new RegExp(
246
- `(?<=\\s|^)${escapeRegex(resolvedValue)}(?=\\s|$)`,
247
- "g"
248
- );
249
-
250
- fixedText = fixedText.replace(pattern, () => {
251
- modified = true;
252
- return `$COMMENT#${commentKey}`;
253
- });
254
- }
255
-
256
- if (modified && fixedText !== comment.value) {
257
- context.report({
258
- loc: comment.loc,
259
- messageId: "message",
260
- fix(fixer) {
261
- const fullCommentText =
262
- comment.type === "Block"
263
- ? `/*${fixedText}*/`
264
- : `//${fixedText}`;
265
- return fixer.replaceText(comment, fullCommentText);
266
- },
267
- });
268
- }
269
- }
270
-
271
- return {};
272
- },
273
- };
274
-
275
- return reverseJsCommentsRule;
276
- };
277
-
278
- async function compressCommentsInProject(fileGlobs = allJSTSFileGlobs) {
279
- const ruleName = "js-comments/js-comments-autofix";
280
-
281
- const eslint = new ESLint({
282
- fix: true,
283
- errorOnUnmatchedPattern: false,
284
- overrideConfigFile: true,
285
- overrideConfig: [
286
- {
287
- files: fileGlobs,
288
- ignores: [...configIgnores, ...knownIgnores], // 🚫 Ensure config isn't linted
289
- languageOptions: typeScriptAndJSXCompatible,
290
- plugins: {
291
- "js-comments": {
292
- rules: {
293
- "js-comments-autofix": makeReverseJsCommentsRule(
294
- reversedFlattenedConfig
295
- ),
296
- },
297
- },
298
- },
299
- rules: {
300
- [ruleName]: "warn", // Don't block builds, just apply fix
301
- },
302
- },
303
- ],
304
- });
305
-
306
- const results = await eslint.lintFiles(fileGlobs);
307
- await ESLint.outputFixes(results);
308
-
309
- console.log({ results });
310
-
311
- const total = results.reduce((sum, r) => {
312
- const add = r.output ? 1 : 0;
313
- return sum + add;
314
- }, 0);
315
- console.log(`✅ Compressed ${total} comment${total === 1 ? "" : "s"}.`);
316
- }
317
-
318
- // ADDRESSES THE CORE COMMANDS "resolve" AND "compress".
319
-
320
- const coreCommand = commands[2];
321
-
322
- switch (coreCommand) {
323
- case "resolve":
324
- await resolveCommentsInProject();
325
- break;
326
- case "compress":
327
- await compressCommentsInProject();
328
- break;
329
- case undefined:
330
- console.log(
331
- `If these settings are correct with you, feel free to initiate the command "resolve" to resolve comments, or "compress" to compress them back to their $COMMENT#* forms.`
332
- );
333
- break;
334
- default:
335
- console.log(
336
- `Core command not recognized. Choose between "resolve" and "compress".`
337
- );
338
- break;
339
- }
340
-
341
- /* Notes
342
- 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.
343
- */
package/test-file.js DELETED
@@ -1,7 +0,0 @@
1
- /*
2
- $COMMENT#LEVELONE#LEVELTWO#LEVELTHREE
3
- $COMMENT#LEVELONE#LEVELTWO#LEVELTHREEBIS
4
- $COMMENT#LEVELONE#LEVELTWO#LEVELTHREETER
5
- $COMMENT#LEVELONE#LEVELTWO#LEVELTHREEALSO
6
- $COMMENT#LEVELONE#LEVELTWO#LEVELTHREETOO
7
- */