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 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
@@ -0,0 +1,5 @@
1
+ import { test2 } from "./import2.js";
2
+
3
+ export const test = test2;
4
+
5
+ // This too is level three.
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
+ }
package/test-file.js ADDED
@@ -0,0 +1,7 @@
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
+ */