comment-variables 0.0.1
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/README.md +1 -0
- package/comments.config.js +20 -0
- package/comments.config.ts +24 -0
- package/find-all-imports.js +231 -0
- package/import.js +5 -0
- package/import2.js +1 -0
- package/index.js +348 -0
- package/package.json +29 -0
- package/run-with-config.js +98 -0
- package/test-file.js +7 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(jscomments in the making)
|
|
@@ -0,0 +1,20 @@
|
|
|
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.
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
*/
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { Linter } from "eslint";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
import { loadConfig, createMatchPath } from "tsconfig-paths";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {readonly [typeof TSX, typeof TS, typeof JSX, typeof JS, typeof MJS, typeof CJS]} Extensions
|
|
10
|
+
* @typedef {import('eslint').Linter.LanguageOptions} LanguageOptions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// JavaScript/TypeScript extensions
|
|
14
|
+
export const TSX = ".tsx";
|
|
15
|
+
export const TS = ".ts";
|
|
16
|
+
export const JSX = ".jsx";
|
|
17
|
+
export const JS = ".js";
|
|
18
|
+
export const MJS = ".mjs";
|
|
19
|
+
export const CJS = ".cjs";
|
|
20
|
+
|
|
21
|
+
// JavaScript/TypeScript extensions array
|
|
22
|
+
/** @type {Extensions} */
|
|
23
|
+
const EXTENSIONS = Object.freeze([TSX, TS, JSX, JS, MJS, CJS]); // In priority order
|
|
24
|
+
|
|
25
|
+
/* resolveImportPath */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds the existing path of an import that does not have an extension specified.
|
|
29
|
+
* @param {string} basePath The absolute import path with extension yet resolved.
|
|
30
|
+
* @returns The absolute path with its extension or `null` if no path is found.
|
|
31
|
+
*/
|
|
32
|
+
const findExistingPath = (basePath) => {
|
|
33
|
+
for (const ext of EXTENSIONS) {
|
|
34
|
+
const fullPath = `${basePath}${ext}`;
|
|
35
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves an import path to a filesystem path, handling:
|
|
42
|
+
* - Base url and aliases (via tsconfig.json `baseUrl` and `paths` compiler options)
|
|
43
|
+
* - Missing extensions (appends `.ts`, `.tsx`, etc.)
|
|
44
|
+
* - Directory imports (e.g., `./components` → `./components/index.ts`)
|
|
45
|
+
* @param {string} currentDir The directory of the file containing the import (such as from `path.dirname(context.filename)`).
|
|
46
|
+
* @param {string} importPath The import specifier (e.g., `@/components/Button` or `./utils`), from the current node.
|
|
47
|
+
* @param {string} cwd The project root (such as from `context.cwd`).
|
|
48
|
+
* @returns The absolute resolved path or `null` if no path is found.
|
|
49
|
+
*/
|
|
50
|
+
export const resolveImportPath = (
|
|
51
|
+
currentDir,
|
|
52
|
+
importPath,
|
|
53
|
+
cwd = process.cwd()
|
|
54
|
+
) => {
|
|
55
|
+
// Step 1: Resolves baseUrl and aliases
|
|
56
|
+
const config = loadConfig(cwd);
|
|
57
|
+
|
|
58
|
+
// creates a function that can resolve paths according to tsconfig's paths property if the config result type is success
|
|
59
|
+
const resolveTSConfig =
|
|
60
|
+
config.resultType === "success"
|
|
61
|
+
? createMatchPath(config.absoluteBaseUrl, config.paths)
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
// resolves a path that relies on tsconfig's baseUrl or aliases if any of the aforementioned are present in the import path
|
|
65
|
+
const baseUrlOrAliasedPath = resolveTSConfig
|
|
66
|
+
? resolveTSConfig(importPath, undefined, undefined, EXTENSIONS)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
// Step 2: Resolves relative/absolute paths
|
|
70
|
+
const basePath = baseUrlOrAliasedPath || path.resolve(currentDir, importPath);
|
|
71
|
+
|
|
72
|
+
// does not resolve on node_modules
|
|
73
|
+
if (basePath.includes("node_modules")) return null;
|
|
74
|
+
|
|
75
|
+
// Case 1: File with extension exists
|
|
76
|
+
if (path.extname(importPath) && fs.existsSync(basePath)) return basePath;
|
|
77
|
+
|
|
78
|
+
// Case 2: Tries appending extensions
|
|
79
|
+
const extensionlessImportPath = findExistingPath(basePath);
|
|
80
|
+
if (extensionlessImportPath) return extensionlessImportPath;
|
|
81
|
+
|
|
82
|
+
// Case 3: Directory import (e.g., `./components` → `./components/index.ts`)
|
|
83
|
+
const indexPath = path.join(basePath, "index");
|
|
84
|
+
const directoryImportPath = findExistingPath(indexPath);
|
|
85
|
+
if (directoryImportPath) return directoryImportPath;
|
|
86
|
+
|
|
87
|
+
return null; // not found
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ESLint configs language options
|
|
91
|
+
const typeScriptAndJSXCompatible = {
|
|
92
|
+
// for compatibility with .ts and .tsx
|
|
93
|
+
parser: tseslint.parser,
|
|
94
|
+
// for compatibility with JSX
|
|
95
|
+
parserOptions: {
|
|
96
|
+
ecmaFeatures: {
|
|
97
|
+
jsx: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/* getSourceCodeFromFilePath */
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the ESLint-generated SourceCode object of a file from its resolved path.
|
|
106
|
+
* @param {string} resolvedPath The resolved path of the file.
|
|
107
|
+
* @param {LanguageOptions} languageOptions The languageOptions object used by `linter.verify()` defaulting to a version that is TypeScript- and JSX-compatible.
|
|
108
|
+
* @returns The ESLint-generated SourceCode object of the file.
|
|
109
|
+
*/
|
|
110
|
+
export const getSourceCodeFromFilePath = (
|
|
111
|
+
resolvedPath,
|
|
112
|
+
languageOptions = typeScriptAndJSXCompatible
|
|
113
|
+
) => {
|
|
114
|
+
// ensures each instance of the function is based on its own linter
|
|
115
|
+
// (just in case somehow some linters were running concurrently)
|
|
116
|
+
const linter = new Linter();
|
|
117
|
+
// the raw code of the file at the end of the resolved path
|
|
118
|
+
const text = fs.readFileSync(resolvedPath, "utf8");
|
|
119
|
+
// utilizes linter.verify ...
|
|
120
|
+
linter.verify(text, { languageOptions });
|
|
121
|
+
// ... to retrieve the raw code as a SourceCode object
|
|
122
|
+
const code = linter.getSourceCode();
|
|
123
|
+
|
|
124
|
+
return code;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/* findAllImports */
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Helper to process and recursively resolve a single import path.
|
|
131
|
+
* Returns false if resolution fails at any level.
|
|
132
|
+
*/
|
|
133
|
+
const processImport = (
|
|
134
|
+
importPath,
|
|
135
|
+
currentDir,
|
|
136
|
+
cwd,
|
|
137
|
+
visited,
|
|
138
|
+
depth,
|
|
139
|
+
maxDepth
|
|
140
|
+
) => {
|
|
141
|
+
const resolvedPath = resolveImportPath(currentDir, importPath, cwd);
|
|
142
|
+
if (!resolvedPath) return true; // Skip unresolved paths (not an error)
|
|
143
|
+
|
|
144
|
+
const result = findAllImports(
|
|
145
|
+
resolvedPath,
|
|
146
|
+
cwd,
|
|
147
|
+
visited,
|
|
148
|
+
depth + 1,
|
|
149
|
+
maxDepth
|
|
150
|
+
);
|
|
151
|
+
return result !== null; // Returns false if child failed
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const findAllImports = (
|
|
155
|
+
filePath,
|
|
156
|
+
cwd = process.cwd(),
|
|
157
|
+
visited = new Set(),
|
|
158
|
+
depth = 0,
|
|
159
|
+
maxDepth = 100
|
|
160
|
+
) => {
|
|
161
|
+
// Early failure checks (with logging)
|
|
162
|
+
if (depth > maxDepth) {
|
|
163
|
+
console.error(`ERROR. Max depth ${maxDepth} reached at ${filePath}.`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (!fs.existsSync(filePath)) {
|
|
167
|
+
console.error(`ERROR. File not found at ${filePath}.`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (visited.has(filePath)) return visited;
|
|
171
|
+
|
|
172
|
+
// Parse AST
|
|
173
|
+
visited.add(filePath);
|
|
174
|
+
const sourceCode = getSourceCodeFromFilePath(filePath);
|
|
175
|
+
if (!sourceCode?.ast) {
|
|
176
|
+
console.error(`ERROR. Failed to parse AST for ${filePath}.`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Process all imports
|
|
181
|
+
const currentDir = path.dirname(filePath);
|
|
182
|
+
for (const node of sourceCode.ast.body) {
|
|
183
|
+
// ES Modules (import x from 'y')
|
|
184
|
+
if (node.type === "ImportDeclaration") {
|
|
185
|
+
if (
|
|
186
|
+
!processImport(
|
|
187
|
+
node.source.value,
|
|
188
|
+
currentDir,
|
|
189
|
+
cwd,
|
|
190
|
+
visited,
|
|
191
|
+
depth,
|
|
192
|
+
maxDepth
|
|
193
|
+
)
|
|
194
|
+
) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// CommonJS (require('x'))
|
|
200
|
+
if (
|
|
201
|
+
node.type === "ExpressionStatement" &&
|
|
202
|
+
node.expression.type === "CallExpression" &&
|
|
203
|
+
node.expression.callee.name === "require" &&
|
|
204
|
+
node.expression.arguments[0]?.type === "Literal"
|
|
205
|
+
) {
|
|
206
|
+
if (
|
|
207
|
+
!processImport(
|
|
208
|
+
node.expression.arguments[0].value,
|
|
209
|
+
currentDir,
|
|
210
|
+
cwd,
|
|
211
|
+
visited,
|
|
212
|
+
depth,
|
|
213
|
+
maxDepth
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return visited; // Success
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/* Notes
|
|
225
|
+
So here I want to make
|
|
226
|
+
- resolveImportPath
|
|
227
|
+
- getSourceCodeFromFilePath (remember the reason I favored the sourceCode is because it grants access to the AST and to the comments.)
|
|
228
|
+
|
|
229
|
+
js-comments is taken on npm.
|
|
230
|
+
JSComments, jscomments is free.
|
|
231
|
+
*/
|
package/import.js
ADDED
package/import2.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const test2 = "test2";
|
package/index.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
|
|
9
|
+
import { runWithConfig } from "./run-with-config.js";
|
|
10
|
+
import { findAllImports } from "./find-all-imports.js";
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
// ENSURES THE CLI TOOL ONLY RUN IN FOLDER THAT POSSESS A package.json FILE AND A .git FOLDER.
|
|
15
|
+
|
|
16
|
+
const hasPackageJson = fs.existsSync(path.join(cwd, "package.json"));
|
|
17
|
+
if (!hasPackageJson) {
|
|
18
|
+
console.error(
|
|
19
|
+
"ERROR. No package.json file found in this directory. Aborting to prevent accidental changes."
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const hasGitFolder = fs.existsSync(path.join(cwd, ".git"));
|
|
24
|
+
if (!hasGitFolder) {
|
|
25
|
+
console.error(
|
|
26
|
+
"ERROR. No git folder found in this directory. Aborting to prevent irreversible changes."
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// GATHERS COMMANDS.
|
|
32
|
+
|
|
33
|
+
const commands = process.argv;
|
|
34
|
+
|
|
35
|
+
// OBTAINS THE VALIDATED FLATTENED CONFIG, REVERSE FLATTENED CONFIG, AND CONFIG PATH.
|
|
36
|
+
|
|
37
|
+
const configFlagIndex = commands.indexOf("--config");
|
|
38
|
+
const passedConfigPath =
|
|
39
|
+
configFlagIndex >= 2 ? path.join(cwd, commands[configFlagIndex + 1]) : null;
|
|
40
|
+
const rawConfigPath = passedConfigPath ?? path.join(cwd, "comments.config.js");
|
|
41
|
+
|
|
42
|
+
const results = await runWithConfig(rawConfigPath);
|
|
43
|
+
if (!results) process.exit(1);
|
|
44
|
+
|
|
45
|
+
const { flattenedConfig, reversedFlattenedConfig, configPath } = results;
|
|
46
|
+
console.log("Config path is:", configPath);
|
|
47
|
+
console.log("Verified flattened config is:", flattenedConfig);
|
|
48
|
+
console.log("Reversed flattened config is:", reversedFlattenedConfig);
|
|
49
|
+
|
|
50
|
+
const keys = new Set([...Object.keys(flattenedConfig)]);
|
|
51
|
+
const values = new Set([...Object.values(flattenedConfig)]);
|
|
52
|
+
|
|
53
|
+
// VALIDATES ONE LAST TIME THE REVERSABILITY OF flattenedConfig AND reversedFlattenedConfig.
|
|
54
|
+
|
|
55
|
+
keys.forEach((key) => {
|
|
56
|
+
if (values.has(key)) {
|
|
57
|
+
console.error(
|
|
58
|
+
`The key "${key}" is and shouldn't be among the values of flattenedConfig.`
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ADDRESSES THE --include-config-imports FLAG, GIVEN THAT THE FILES IMPORTED BY THE CONFIG ARE IGNORED BY DEFAULT.
|
|
65
|
+
|
|
66
|
+
const includeConfigImports = commands.indexOf("--include-config-imports") >= 2;
|
|
67
|
+
const rawConfigIgnores = includeConfigImports
|
|
68
|
+
? [configPath]
|
|
69
|
+
: [...findAllImports(configPath)];
|
|
70
|
+
|
|
71
|
+
// the ignore paths must be relative
|
|
72
|
+
const configIgnores = rawConfigIgnores.map((e) => path.relative(cwd, e));
|
|
73
|
+
console.log(
|
|
74
|
+
includeConfigImports ? "Config ignore is:" : "Config ignores are",
|
|
75
|
+
configIgnores
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// DEFINES DEFAULT ESLINT IGNORES AND FILES.
|
|
79
|
+
|
|
80
|
+
const knownIgnores = [
|
|
81
|
+
".next",
|
|
82
|
+
".react-router",
|
|
83
|
+
"node_modules",
|
|
84
|
+
".parcel-cache",
|
|
85
|
+
".react-router-parcel",
|
|
86
|
+
"dist",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const allJSTSFileGlobs = [
|
|
90
|
+
"**/*.tsx",
|
|
91
|
+
"**/*.ts",
|
|
92
|
+
"**/*.jsx",
|
|
93
|
+
"**/*.js",
|
|
94
|
+
"**/*.mjs",
|
|
95
|
+
"**/*.cjs",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// MAKES THE FLOW FOR resolveCommentsInProject.
|
|
99
|
+
|
|
100
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
|
|
101
|
+
const jsCommentsRule = {
|
|
102
|
+
meta: {
|
|
103
|
+
type: "suggestion",
|
|
104
|
+
docs: {
|
|
105
|
+
description: "Resolve $COMMENT#... using js-comments config.",
|
|
106
|
+
},
|
|
107
|
+
messages: {
|
|
108
|
+
message: `Resolved $COMMENT placeholder(s) in comment.`,
|
|
109
|
+
},
|
|
110
|
+
fixable: "code",
|
|
111
|
+
schema: [],
|
|
112
|
+
},
|
|
113
|
+
create: (context) => {
|
|
114
|
+
const sourceCode = context.sourceCode;
|
|
115
|
+
const comments = sourceCode
|
|
116
|
+
.getAllComments()
|
|
117
|
+
.filter((e) => e.type !== "Shebang");
|
|
118
|
+
|
|
119
|
+
for (const comment of comments) {
|
|
120
|
+
const matches = [...comment.value.matchAll(/\$COMMENT#([A-Z0-9#_]+)/g)];
|
|
121
|
+
|
|
122
|
+
if (matches.length === 0) continue;
|
|
123
|
+
|
|
124
|
+
let fixedText = comment.value;
|
|
125
|
+
let hasValidFix = false;
|
|
126
|
+
|
|
127
|
+
for (const match of matches) {
|
|
128
|
+
const fullMatch = match[0]; // e.g. $COMMENT#LEVELONE#LEVELTWO
|
|
129
|
+
const key = match[1]; // e.g. LEVELONE#LEVELTWO
|
|
130
|
+
const replacement = flattenedConfig[key];
|
|
131
|
+
|
|
132
|
+
if (replacement) {
|
|
133
|
+
fixedText = fixedText.replace(fullMatch, replacement);
|
|
134
|
+
hasValidFix = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (hasValidFix && fixedText !== comment.value) {
|
|
139
|
+
context.report({
|
|
140
|
+
loc: comment.loc,
|
|
141
|
+
messageId: "message",
|
|
142
|
+
fix(fixer) {
|
|
143
|
+
const range = comment.range;
|
|
144
|
+
const prefix = comment.type === "Block" ? "/*" : "//";
|
|
145
|
+
const suffix = comment.type === "Block" ? "*/" : "";
|
|
146
|
+
const newComment = `${prefix}${fixedText}${suffix}`;
|
|
147
|
+
|
|
148
|
+
return fixer.replaceTextRange(range, newComment);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
async function resolveCommentsInProject(fileGlobs = allJSTSFileGlobs) {
|
|
159
|
+
const ruleName = "js-comments/js-comments-autofix";
|
|
160
|
+
|
|
161
|
+
const eslint = new ESLint({
|
|
162
|
+
fix: true,
|
|
163
|
+
errorOnUnmatchedPattern: false,
|
|
164
|
+
overrideConfigFile: true,
|
|
165
|
+
overrideConfig: [
|
|
166
|
+
{
|
|
167
|
+
files: fileGlobs,
|
|
168
|
+
ignores: [...configIgnores, ...knownIgnores], // 🚫 Ensure config isn't linted
|
|
169
|
+
languageOptions: {
|
|
170
|
+
ecmaVersion: "latest",
|
|
171
|
+
sourceType: "module",
|
|
172
|
+
},
|
|
173
|
+
plugins: {
|
|
174
|
+
"js-comments": {
|
|
175
|
+
rules: {
|
|
176
|
+
"js-comments-autofix": jsCommentsRule,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
rules: {
|
|
181
|
+
[ruleName]: "warn", // Don't block builds, just apply fix
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const results = await eslint.lintFiles(fileGlobs);
|
|
188
|
+
await ESLint.outputFixes(results);
|
|
189
|
+
|
|
190
|
+
console.log({ results });
|
|
191
|
+
|
|
192
|
+
const total = results.reduce((sum, r) => {
|
|
193
|
+
const add = r.output ? 1 : 0;
|
|
194
|
+
return sum + add;
|
|
195
|
+
}, 0);
|
|
196
|
+
console.log(`✅ Resolved ${total} comment${total === 1 ? "" : "s"}.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// MAKES THE FLOW FOR compressCommentsInProject.
|
|
200
|
+
|
|
201
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
202
|
+
|
|
203
|
+
const makeReverseJsCommentsRule = (reversedFlattenedConfig) => {
|
|
204
|
+
// Sort the resolved values by descending length to prevent partial replacements.
|
|
205
|
+
const sortedReversedFlattenedConfig = Object.entries(
|
|
206
|
+
reversedFlattenedConfig
|
|
207
|
+
).sort(([a], [b]) => b.length - a.length);
|
|
208
|
+
|
|
209
|
+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule<string, []>} */
|
|
210
|
+
const reverseJsCommentsRule = {
|
|
211
|
+
meta: {
|
|
212
|
+
type: "suggestion",
|
|
213
|
+
docs: {
|
|
214
|
+
description: "Resolve $COMMENT#... using js-comments config in reverse",
|
|
215
|
+
},
|
|
216
|
+
messages: {
|
|
217
|
+
message: `Comment compressed.`,
|
|
218
|
+
},
|
|
219
|
+
fixable: "code",
|
|
220
|
+
schema: [],
|
|
221
|
+
},
|
|
222
|
+
create(context) {
|
|
223
|
+
const sourceCode = context.sourceCode;
|
|
224
|
+
const comments = sourceCode
|
|
225
|
+
.getAllComments()
|
|
226
|
+
.filter((e) => e.type !== "Shebang");
|
|
227
|
+
|
|
228
|
+
for (const comment of comments) {
|
|
229
|
+
let fixedText = comment.value;
|
|
230
|
+
let modified = false;
|
|
231
|
+
|
|
232
|
+
for (const [
|
|
233
|
+
resolvedValue,
|
|
234
|
+
commentKey,
|
|
235
|
+
] of sortedReversedFlattenedConfig) {
|
|
236
|
+
// if (fixedText.includes(resolvedValue)) {
|
|
237
|
+
// fixedText = fixedText.replaceAll(
|
|
238
|
+
// resolvedValue,
|
|
239
|
+
// `$COMMENT#${commentKey}`
|
|
240
|
+
// );
|
|
241
|
+
// modified = true;
|
|
242
|
+
// }
|
|
243
|
+
// }
|
|
244
|
+
|
|
245
|
+
// if (modified) {
|
|
246
|
+
|
|
247
|
+
const pattern = new RegExp(
|
|
248
|
+
`(?<=\\s|^)${escapeRegex(resolvedValue)}(?=\\s|$)`,
|
|
249
|
+
"g"
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
fixedText = fixedText.replace(pattern, () => {
|
|
253
|
+
modified = true;
|
|
254
|
+
return `$COMMENT#${commentKey}`;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (modified && fixedText !== comment.value) {
|
|
259
|
+
context.report({
|
|
260
|
+
loc: comment.loc,
|
|
261
|
+
messageId: "message",
|
|
262
|
+
fix(fixer) {
|
|
263
|
+
const fullCommentText =
|
|
264
|
+
comment.type === "Block"
|
|
265
|
+
? `/*${fixedText}*/`
|
|
266
|
+
: `//${fixedText}`;
|
|
267
|
+
return fixer.replaceText(comment, fullCommentText);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {};
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return reverseJsCommentsRule;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
async function compressCommentsInProject(fileGlobs = allJSTSFileGlobs) {
|
|
281
|
+
const ruleName = "js-comments/js-comments-autofix";
|
|
282
|
+
|
|
283
|
+
const eslint = new ESLint({
|
|
284
|
+
fix: true,
|
|
285
|
+
errorOnUnmatchedPattern: false,
|
|
286
|
+
overrideConfigFile: true,
|
|
287
|
+
overrideConfig: [
|
|
288
|
+
{
|
|
289
|
+
files: fileGlobs,
|
|
290
|
+
ignores: [...configIgnores, ...knownIgnores], // 🚫 Ensure config isn't linted
|
|
291
|
+
languageOptions: {
|
|
292
|
+
ecmaVersion: "latest",
|
|
293
|
+
sourceType: "module",
|
|
294
|
+
},
|
|
295
|
+
plugins: {
|
|
296
|
+
"js-comments": {
|
|
297
|
+
rules: {
|
|
298
|
+
"js-comments-autofix": makeReverseJsCommentsRule(
|
|
299
|
+
reversedFlattenedConfig
|
|
300
|
+
),
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
rules: {
|
|
305
|
+
[ruleName]: "warn", // Don't block builds, just apply fix
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const results = await eslint.lintFiles(fileGlobs);
|
|
312
|
+
await ESLint.outputFixes(results);
|
|
313
|
+
|
|
314
|
+
console.log({ results });
|
|
315
|
+
|
|
316
|
+
const total = results.reduce((sum, r) => {
|
|
317
|
+
const add = r.output ? 1 : 0;
|
|
318
|
+
return sum + add;
|
|
319
|
+
}, 0);
|
|
320
|
+
console.log(`✅ Compressed ${total} comment${total === 1 ? "" : "s"}.`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ADDRESSES THE CORE COMMANDS "resolve" AND "compress".
|
|
324
|
+
|
|
325
|
+
const coreCommand = commands[2];
|
|
326
|
+
|
|
327
|
+
switch (coreCommand) {
|
|
328
|
+
case "resolve":
|
|
329
|
+
await resolveCommentsInProject();
|
|
330
|
+
break;
|
|
331
|
+
case "compress":
|
|
332
|
+
await compressCommentsInProject();
|
|
333
|
+
break;
|
|
334
|
+
case undefined:
|
|
335
|
+
console.log(
|
|
336
|
+
`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.`
|
|
337
|
+
);
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
console.log(
|
|
341
|
+
`Core command not recognized. Choose between "resolve" and "compress".`
|
|
342
|
+
);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Notes
|
|
347
|
+
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.
|
|
348
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "comment-variables",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"bin": {
|
|
6
|
+
"jscomments": "./index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/LutherTS/jscomments.git"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"eslint": "^9.29.0",
|
|
22
|
+
"tsconfig-paths": "^4.2.0",
|
|
23
|
+
"typescript-eslint": "^8.34.1",
|
|
24
|
+
"zod": "^3.25.67"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@typescript-eslint/utils": "^8.34.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
function flattenConfig(
|
|
7
|
+
config,
|
|
8
|
+
normalizedPath = "",
|
|
9
|
+
map = {},
|
|
10
|
+
pathStack = [],
|
|
11
|
+
reversedFlattenedConfig = {}
|
|
12
|
+
) {
|
|
13
|
+
for (const [key, value] of Object.entries(config)) {
|
|
14
|
+
const currentPath = [...pathStack, key];
|
|
15
|
+
normalizedPath = currentPath
|
|
16
|
+
.map((k) => k.toUpperCase())
|
|
17
|
+
.join("#")
|
|
18
|
+
.replaceAll(" ", "_"); // spaces are replaced by underscores
|
|
19
|
+
|
|
20
|
+
if (typeof value === "string") {
|
|
21
|
+
if (map[normalizedPath]) {
|
|
22
|
+
// checks that no two keys are duplicate
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Duplicate normalized key detected: "${normalizedPath}".\nConflict between:\n - ${
|
|
25
|
+
map[normalizedPath].__source
|
|
26
|
+
}\n - ${currentPath.join(" > ")}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
map[normalizedPath] = {
|
|
30
|
+
value,
|
|
31
|
+
__source: currentPath.join(" > "), // for debugging
|
|
32
|
+
};
|
|
33
|
+
} else if (typeof value === "object") {
|
|
34
|
+
flattenConfig(value, normalizedPath, map, currentPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const flattenedConfig = Object.fromEntries(
|
|
39
|
+
Object.entries(map).map(([k, v]) => [k, v.value])
|
|
40
|
+
); // strip metadata
|
|
41
|
+
|
|
42
|
+
const set = new Set();
|
|
43
|
+
|
|
44
|
+
// the integrity of the config needs to be established before working with it
|
|
45
|
+
for (const value of Object.values(flattenedConfig)) {
|
|
46
|
+
if (set.has(value)) {
|
|
47
|
+
// checks that no two values are duplicate
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Value "${value}" is already assigned to an existing key.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
set.add(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [key, value] of Object.entries(flattenedConfig)) {
|
|
56
|
+
reversedFlattenedConfig[value] = key;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { flattenedConfig, reversedFlattenedConfig };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runWithConfig(rawConfigPath) {
|
|
63
|
+
// const __filename = fileURLToPath(import.meta.url);
|
|
64
|
+
// const __dirname = dirname(__filename);
|
|
65
|
+
|
|
66
|
+
// const configPath = join(__dirname, rawConfigPath);
|
|
67
|
+
const configPath = rawConfigPath;
|
|
68
|
+
|
|
69
|
+
// Step 1: Check if config file exists
|
|
70
|
+
if (!existsSync(configPath)) {
|
|
71
|
+
console.warn("No config file found. Exiting gracefully.");
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 2: Import the config dynamically
|
|
76
|
+
const configModule = await import(pathToFileURL(configPath));
|
|
77
|
+
const config = configModule.default;
|
|
78
|
+
|
|
79
|
+
// Step 3: Validate config object
|
|
80
|
+
if (!config || typeof config !== "object") {
|
|
81
|
+
console.warn("Invalid config format. Exiting.");
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const RecursiveObject = z.lazy(() =>
|
|
86
|
+
z.record(z.union([z.string(), RecursiveObject]))
|
|
87
|
+
);
|
|
88
|
+
const result = RecursiveObject.safeParse(config);
|
|
89
|
+
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
console.warn("Config could not pass validation from zod.");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 4: Do your thing
|
|
96
|
+
console.log("Running with config:", config);
|
|
97
|
+
return { ...flattenConfig(config), configPath };
|
|
98
|
+
}
|