jsdoczoom 0.2.1 → 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,3 +1,5 @@
1
+ import { appendText, DESCRIPTION_TAGS } from "./types.js";
2
+
1
3
  /**
2
4
  * Custom ESLint plugin for jsdoczoom file-level JSDoc validation.
3
5
  *
@@ -8,13 +10,6 @@
8
10
  *
9
11
  * @summary Custom ESLint plugin with file-level JSDoc validation rules
10
12
  */
11
- /** Tags whose content is treated as description (free-text). */
12
- const DESCRIPTION_TAGS = new Set([
13
- "desc",
14
- "description",
15
- "file",
16
- "fileoverview",
17
- ]);
18
13
  /**
19
14
  * Find the file-level JSDoc block comment from the source code.
20
15
  *
@@ -51,13 +46,6 @@ function findFileJsdocComment(sourceCode, programBody) {
51
46
  // If no non-import statements, return first JSDoc comment in file
52
47
  return jsdocComments[0] ?? null;
53
48
  }
54
- /**
55
- * Append non-empty text to an accumulator with space separation.
56
- */
57
- function appendText(existing, addition) {
58
- if (!addition) return existing;
59
- return existing ? `${existing} ${addition}` : addition;
60
- }
61
49
  /**
62
50
  * Parse the inner content of a JSDoc block comment.
63
51
  *
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import ts from "typescript";
3
3
  import { JsdocError } from "./errors.js";
4
+ import { appendText, DESCRIPTION_TAGS } from "./types.js";
4
5
  /**
5
6
  * Uses the TypeScript compiler to locate the first JSDoc block before any
6
7
  * code statements, then extracts the first `@summary` tag and free-text
@@ -123,21 +124,6 @@ function getDiagnostics(sourceFile) {
123
124
  if (!diags || diags.length === 0) return [];
124
125
  return diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
125
126
  }
126
- /**
127
- * Append non-empty text to an accumulator with space separation.
128
- */
129
- function appendText(existing, addition) {
130
- if (addition.length === 0) return existing;
131
- if (existing.length === 0) return addition;
132
- return `${existing} ${addition}`;
133
- }
134
- /** Tags whose content is treated as description (free-text). */
135
- const DESCRIPTION_TAGS = new Set([
136
- "desc",
137
- "description",
138
- "file",
139
- "fileoverview",
140
- ]);
141
127
  /**
142
128
  * Parse @summary tag and free-text description from raw JSDoc inner text.
143
129
  *
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import ts from "typescript";
3
4
  import { JsdocError } from "./errors.js";
5
+
4
6
  /**
5
7
  * Produces .d.ts-like output from a TypeScript source file using the
6
8
  * TypeScript compiler's declaration emit. Preserves JSDoc comments and
@@ -9,6 +11,145 @@ import { JsdocError } from "./errors.js";
9
11
  *
10
12
  * @summary Generate TypeScript declaration output from source files
11
13
  */
14
+ // Cache for resolved compiler options, keyed by tsconfig path
15
+ const compilerOptionsCache = new Map();
16
+ // Shared document registry for caching parsed source files across language services
17
+ const documentRegistry = ts.createDocumentRegistry();
18
+ // Cache for language services, keyed by tsconfig path
19
+ const serviceCache = new Map();
20
+ /**
21
+ * Resolves compiler options for a given file path by finding and parsing
22
+ * the nearest tsconfig.json file.
23
+ *
24
+ * @internal
25
+ * @param filePath - Absolute path to the TypeScript source file
26
+ * @returns Object containing the tsconfig path and resolved compiler options
27
+ */
28
+ export function resolveCompilerOptions(filePath) {
29
+ // Required overrides that must always be present
30
+ const requiredOverrides = {
31
+ declaration: true,
32
+ emitDeclarationOnly: true,
33
+ removeComments: false,
34
+ skipLibCheck: true,
35
+ };
36
+ // Fallback defaults when no tsconfig is found
37
+ const fallbackDefaults = {
38
+ ...requiredOverrides,
39
+ target: ts.ScriptTarget.Latest,
40
+ module: ts.ModuleKind.NodeNext,
41
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
42
+ };
43
+ // Try to find the nearest tsconfig.json
44
+ const tsconfigPath = ts.findConfigFile(
45
+ dirname(filePath),
46
+ ts.sys.fileExists,
47
+ "tsconfig.json",
48
+ );
49
+ // Use cache key based on tsconfig path (or "__default__" if none found)
50
+ const cacheKey = tsconfigPath ?? "__default__";
51
+ if (compilerOptionsCache.has(cacheKey)) {
52
+ return compilerOptionsCache.get(cacheKey);
53
+ }
54
+ let result;
55
+ if (!tsconfigPath) {
56
+ // No tsconfig found - use fallback defaults
57
+ result = {
58
+ tsconfigPath: null,
59
+ options: fallbackDefaults,
60
+ };
61
+ } else {
62
+ // Try to parse the tsconfig
63
+ try {
64
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
65
+ if (configFile.error) {
66
+ // Malformed JSON - fall back to defaults
67
+ result = {
68
+ tsconfigPath: null,
69
+ options: fallbackDefaults,
70
+ };
71
+ } else {
72
+ // Parse the config content
73
+ const parsedConfig = ts.parseJsonConfigFileContent(
74
+ configFile.config,
75
+ ts.sys,
76
+ dirname(tsconfigPath),
77
+ );
78
+ // Merge parsed options with required overrides
79
+ result = {
80
+ tsconfigPath,
81
+ options: {
82
+ ...parsedConfig.options,
83
+ ...requiredOverrides,
84
+ },
85
+ };
86
+ }
87
+ } catch (_error) {
88
+ // Error reading or parsing tsconfig - fall back to defaults
89
+ // Note: This catches file system errors (EACCES, ENOENT) and any unexpected
90
+ // errors from ts.readConfigFile or ts.parseJsonConfigFileContent
91
+ result = {
92
+ tsconfigPath: null,
93
+ options: fallbackDefaults,
94
+ };
95
+ }
96
+ }
97
+ compilerOptionsCache.set(cacheKey, result);
98
+ return result;
99
+ }
100
+ /**
101
+ * Gets or creates a cached language service for the given tsconfig and compiler options.
102
+ * Language services are shared across files in the same project (same tsconfig path) and
103
+ * reuse parsed source files via the document registry.
104
+ *
105
+ * @internal
106
+ * @param tsconfigPath - Path to the tsconfig.json file, or null if using defaults
107
+ * @param compilerOptions - Resolved compiler options for this project
108
+ * @returns Object containing the language service and mutable set of files
109
+ */
110
+ export function getLanguageService(tsconfigPath, compilerOptions) {
111
+ const cacheKey = tsconfigPath ?? "__default__";
112
+ if (serviceCache.has(cacheKey)) {
113
+ return serviceCache.get(cacheKey);
114
+ }
115
+ // Create a mutable set to track files for this service
116
+ const files = new Set();
117
+ // Create a language service host
118
+ const host = {
119
+ getScriptFileNames: () => Array.from(files),
120
+ getScriptVersion: (_fileName) => "0", // Static version - no watch mode
121
+ getScriptSnapshot: (fileName) => {
122
+ const content = ts.sys.readFile(fileName);
123
+ if (content === undefined) {
124
+ return undefined;
125
+ }
126
+ return ts.ScriptSnapshot.fromString(content);
127
+ },
128
+ getCompilationSettings: () => compilerOptions,
129
+ getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
130
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
131
+ fileExists: ts.sys.fileExists,
132
+ readFile: ts.sys.readFile,
133
+ readDirectory: ts.sys.readDirectory,
134
+ directoryExists: ts.sys.directoryExists,
135
+ getDirectories: ts.sys.getDirectories,
136
+ };
137
+ // Create the language service with the document registry for caching
138
+ const service = ts.createLanguageService(host, documentRegistry);
139
+ const cacheEntry = { service, files };
140
+ serviceCache.set(cacheKey, cacheEntry);
141
+ return cacheEntry;
142
+ }
143
+ /**
144
+ * Resets all caches (compiler options, language services, and document registry).
145
+ * Used for test isolation to ensure a clean state between test runs.
146
+ *
147
+ * @internal
148
+ */
149
+ export function resetCache() {
150
+ compilerOptionsCache.clear();
151
+ serviceCache.clear();
152
+ }
12
153
  /**
13
154
  * Generates TypeScript declaration output from a source file.
14
155
  *
@@ -29,86 +170,39 @@ import { JsdocError } from "./errors.js";
29
170
  * @throws {JsdocError} If the file cannot be read or parsed
30
171
  */
31
172
  export function generateTypeDeclarations(filePath) {
32
- let sourceText;
173
+ // Verify the file exists and throw FILE_NOT_FOUND for any read errors
33
174
  try {
34
- sourceText = readFileSync(filePath, "utf-8");
175
+ readFileSync(filePath, "utf-8");
35
176
  } catch (_error) {
36
177
  throw new JsdocError("FILE_NOT_FOUND", `Failed to read file: ${filePath}`);
37
178
  }
38
- // Create compiler options matching the project setup
39
- const compilerOptions = {
40
- declaration: true,
41
- emitDeclarationOnly: true,
42
- target: ts.ScriptTarget.Latest,
43
- module: ts.ModuleKind.NodeNext,
44
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
45
- skipLibCheck: true,
46
- removeComments: false, // Preserve JSDoc comments
47
- };
48
- // Create a custom compiler host that provides our file
49
- const host = ts.createCompilerHost(compilerOptions);
50
- const originalGetSourceFile = host.getSourceFile;
51
- host.getSourceFile = (
52
- fileName,
53
- languageVersion,
54
- onError,
55
- shouldCreateNewSourceFile,
56
- ) => {
57
- if (fileName === filePath) {
58
- return ts.createSourceFile(fileName, sourceText, languageVersion, true);
59
- }
60
- return originalGetSourceFile.call(
61
- host,
62
- fileName,
63
- languageVersion,
64
- onError,
65
- shouldCreateNewSourceFile,
66
- );
67
- };
68
- // Capture emitted output
69
- let declarationOutput = "";
70
- host.writeFile = (fileName, data) => {
71
- if (fileName.endsWith(".d.ts")) {
72
- declarationOutput = data;
73
- }
74
- };
75
- // Create program and emit declarations
76
- const program = ts.createProgram([filePath], compilerOptions, host);
77
- const sourceFile = program.getSourceFile(filePath);
78
- if (!sourceFile) {
79
- throw new JsdocError("PARSE_ERROR", `Failed to parse file: ${filePath}`);
80
- }
81
- const emitResult = program.emit(sourceFile, undefined, undefined, true);
82
- // Check for emit errors
83
- const diagnostics = ts
84
- .getPreEmitDiagnostics(program)
85
- .concat(emitResult.diagnostics);
179
+ // Resolve compiler options from nearest tsconfig.json
180
+ const { tsconfigPath, options } = resolveCompilerOptions(filePath);
181
+ // Get or create a cached language service for this project
182
+ const { service, files } = getLanguageService(tsconfigPath, options);
183
+ // Register the file with the language service
184
+ files.add(filePath);
185
+ // Check for parse errors first
186
+ const diagnostics = service.getSyntacticDiagnostics(filePath);
86
187
  if (diagnostics.length > 0) {
87
- const errors = diagnostics
88
- .map((diagnostic) => {
89
- if (diagnostic.file && diagnostic.start !== undefined) {
90
- const { line, character } =
91
- diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
92
- const message = ts.flattenDiagnosticMessageText(
93
- diagnostic.messageText,
94
- "\n",
95
- );
96
- return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
97
- }
98
- return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
99
- })
100
- .join("\n");
101
- throw new JsdocError("PARSE_ERROR", `TypeScript errors:\n${errors}`);
188
+ throw new JsdocError("PARSE_ERROR", `Failed to parse file: ${filePath}`);
102
189
  }
103
- if (!declarationOutput) {
104
- // If no output was generated, the file may have no exports
105
- // Return empty string in this case
190
+ // Get emit output using the language service
191
+ const emitOutput = service.getEmitOutput(filePath, true); // true = emitOnlyDtsFiles
192
+ // Find the .d.ts output file
193
+ const dtsFile = emitOutput.outputFiles.find((file) =>
194
+ file.name.endsWith(".d.ts"),
195
+ );
196
+ if (!dtsFile) {
197
+ // No declaration output - file has no exports
106
198
  return "";
107
199
  }
108
200
  // Clean up the output
109
- let cleaned = declarationOutput;
201
+ let cleaned = dtsFile.text;
110
202
  // Remove empty export statement if present and no other exports
111
- if (cleaned.trim() === "export {};") {
203
+ // Strip out any leading comments first to check if the only actual code is "export {};"
204
+ const withoutComments = cleaned.replace(/\/\*\*[\s\S]*?\*\//g, "").trim();
205
+ if (withoutComments === "export {};") {
112
206
  cleaned = "";
113
207
  }
114
208
  return cleaned;
package/dist/types.js CHANGED
@@ -14,3 +14,18 @@ export const VALIDATION_STATUS_PRIORITY = [
14
14
  "missing_description",
15
15
  "missing_barrel",
16
16
  ];
17
+ /** Tags whose content is treated as description (free-text). */
18
+ export const DESCRIPTION_TAGS = new Set([
19
+ "desc",
20
+ "description",
21
+ "file",
22
+ "fileoverview",
23
+ ]);
24
+ /**
25
+ * Append non-empty text to an accumulator with space separation.
26
+ */
27
+ export function appendText(existing, addition) {
28
+ if (addition.length === 0) return existing;
29
+ if (existing.length === 0) return addition;
30
+ return `${existing} ${addition}`;
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsdoczoom",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for extracting JSDoc summaries at configurable depths",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,4 +1,4 @@
1
- import type { ParsedFileInfo } from "./types.js";
1
+ import { type ParsedFileInfo } from "./types.js";
2
2
  /**
3
3
  * Uses the TypeScript compiler to locate the first JSDoc block before any
4
4
  * code statements, then extracts the first `@summary` tag and free-text
@@ -1,11 +1,40 @@
1
+ import ts from "typescript";
1
2
  /**
2
- * Produces .d.ts-like output from a TypeScript source file using the
3
- * TypeScript compiler's declaration emit. Preserves JSDoc comments and
4
- * source order while stripping implementation bodies and non-exported
5
- * internals.
3
+ * Resolves compiler options for a given file path by finding and parsing
4
+ * the nearest tsconfig.json file.
6
5
  *
7
- * @summary Generate TypeScript declaration output from source files
6
+ * @internal
7
+ * @param filePath - Absolute path to the TypeScript source file
8
+ * @returns Object containing the tsconfig path and resolved compiler options
9
+ */
10
+ export declare function resolveCompilerOptions(filePath: string): {
11
+ tsconfigPath: string | null;
12
+ options: ts.CompilerOptions;
13
+ };
14
+ /**
15
+ * Gets or creates a cached language service for the given tsconfig and compiler options.
16
+ * Language services are shared across files in the same project (same tsconfig path) and
17
+ * reuse parsed source files via the document registry.
18
+ *
19
+ * @internal
20
+ * @param tsconfigPath - Path to the tsconfig.json file, or null if using defaults
21
+ * @param compilerOptions - Resolved compiler options for this project
22
+ * @returns Object containing the language service and mutable set of files
23
+ */
24
+ export declare function getLanguageService(
25
+ tsconfigPath: string | null,
26
+ compilerOptions: ts.CompilerOptions,
27
+ ): {
28
+ service: ts.LanguageService;
29
+ files: Set<string>;
30
+ };
31
+ /**
32
+ * Resets all caches (compiler options, language services, and document registry).
33
+ * Used for test isolation to ensure a clean state between test runs.
34
+ *
35
+ * @internal
8
36
  */
37
+ export declare function resetCache(): void;
9
38
  /**
10
39
  * Generates TypeScript declaration output from a source file.
11
40
  *
package/types/types.d.ts CHANGED
@@ -81,6 +81,12 @@ export interface SelectorInfo {
81
81
  pattern: string;
82
82
  depth: number | undefined;
83
83
  }
84
+ /** Tags whose content is treated as description (free-text). */
85
+ export declare const DESCRIPTION_TAGS: Set<string>;
86
+ /**
87
+ * Append non-empty text to an accumulator with space separation.
88
+ */
89
+ export declare function appendText(existing: string, addition: string): string;
84
90
  /** A single lint diagnostic from ESLint */
85
91
  export interface LintDiagnostic {
86
92
  line: number;