@player-tools/metrics-output-plugin 0.13.0--canary.231.5583 → 0.13.0--canary.231.5678

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.
@@ -85,6 +85,9 @@ var MetricsOutput = class {
85
85
  loadExistingMetrics() {
86
86
  try {
87
87
  const fileContent = fs.readFileSync(this.outputFilePath, "utf-8");
88
+ if (!fileContent.trim()) {
89
+ return;
90
+ }
88
91
  const parsed = JSON.parse(fileContent);
89
92
  const existingMetrics = parsed && typeof parsed === "object" ? parsed : {};
90
93
  this.aggregatedResults = merge(
@@ -93,7 +96,7 @@ var MetricsOutput = class {
93
96
  );
94
97
  } catch (error) {
95
98
  console.warn(
96
- `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`
99
+ `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`
97
100
  );
98
101
  }
99
102
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/index.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/metrics-output.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/utils.ts"],"sourcesContent":["export * from \"./metrics-output\";\nexport * from \"./utils\";\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { merge as deepMerge } from \"ts-deepmerge\";\nimport { Diagnostic } from \"vscode-languageserver-types\";\nimport type {\n PlayerLanguageService,\n PlayerLanguageServicePlugin,\n DocumentContext,\n} from \"@player-tools/json-language-service\";\nimport type {\n MetricsRoot,\n MetricsStats,\n MetricsFeatures,\n MetricsContent,\n MetricsReport,\n MetricValue,\n} from \"./types\";\n\nexport interface MetricsOutputConfig {\n /** Directory where the output file will be written */\n outputDir?: string;\n\n /** Name of the JSON output file */\n fileName?: string;\n\n /**\n * Custom properties to include at the root level of the output\n */\n rootProperties?: MetricsRoot;\n\n /**\n * Content-specific stats\n */\n stats?: MetricsStats;\n\n /**\n * Content-specific features\n */\n features?: MetricsFeatures;\n}\n\n/**\n * Normalizes a file path to use consistent separators and format\n */\nfunction normalizePath(filePath: string): string {\n // Convert backslashes to forward slashes for consistency\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Remove file:// protocol if present\n return normalized.replace(/^file:\\/\\//, \"\");\n}\n\n// Narrow ts-deepmerge’s generic return type to what's needed\nconst merge = deepMerge as <T>(...objects: Array<Partial<T>>) => T;\n\n/**\n * A plugin that writes diagnostic results to a JSON file in a specified output directory.\n * NOTE: This plugin is designed for CLI usage only and should not be used in an IDE.\n */\nexport class MetricsOutput implements PlayerLanguageServicePlugin {\n name = \"metrics-output-plugin\";\n\n private outputDir: string;\n private fileName: string;\n private rootProperties: MetricsRoot;\n private stats: MetricsStats;\n private features: MetricsFeatures;\n\n // In-memory storage of all results\n private aggregatedResults: MetricsReport = {\n content: {},\n };\n\n private get outputFilePath(): string {\n return path.resolve(this.outputDir, `${this.fileName}.json`);\n }\n\n constructor(options: MetricsOutputConfig = {}) {\n this.outputDir = options.outputDir || process.cwd();\n\n // Handle file name, stripping .json extension if provided\n let fileName = options.fileName || \"metrics\";\n if (fileName.endsWith(\".json\")) {\n fileName = fileName.split(\".\")[0]; // Remove extension\n }\n this.fileName = fileName;\n this.rootProperties = options.rootProperties || {};\n this.stats = options.stats || {};\n this.features = options.features || {};\n }\n\n apply(service: PlayerLanguageService): void {\n // Hook into the validation end to capture diagnostics and write output\n service.hooks.onValidateEnd.tap(\n this.name,\n (\n diagnostics: Diagnostic[],\n { documentContext }: { documentContext: DocumentContext },\n ): Diagnostic[] => {\n // If metrics file exists, load and append to it\n if (fs.existsSync(this.outputFilePath)) {\n this.loadExistingMetrics();\n }\n\n this.generateFile(diagnostics, documentContext);\n\n return diagnostics;\n },\n );\n }\n\n private loadExistingMetrics(): void {\n try {\n const fileContent = fs.readFileSync(this.outputFilePath, \"utf-8\");\n const parsed: unknown = JSON.parse(fileContent);\n const existingMetrics: Partial<MetricsReport> =\n parsed && typeof parsed === \"object\"\n ? (parsed as Partial<MetricsReport>)\n : {};\n\n // Recursively merge existing metrics with current aggregated results\n this.aggregatedResults = merge<MetricsReport>(\n existingMetrics,\n this.aggregatedResults,\n );\n } catch (error) {\n // If we can't parse existing file, continue with current state\n console.warn(\n `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`,\n );\n }\n }\n\n /**\n * Evaluates a value, executing it if it's a function\n */\n private evaluateValue(\n value: MetricValue,\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ) {\n if (typeof value === \"function\") {\n try {\n return value(diagnostics, documentContext);\n } catch (error) {\n documentContext.log.error(`Error evaluating value: ${error}`);\n return { error: `Value evaluation failed: ${error}` };\n }\n }\n return value;\n }\n\n private generateMetrics(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): MetricsStats {\n const statsSource = this.stats;\n // If stats is a function, evaluate it directly\n if (typeof statsSource === \"function\") {\n try {\n const result = statsSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicStatsValue: result };\n } catch (error) {\n documentContext.log.error(`Error evaluating stats function: ${error}`);\n return { error: `Stats function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each metric in the record\n const result: MetricsStats = {};\n Object.entries(statsSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFeatures(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): Record<string, MetricValue> {\n const featuresSource = this.features;\n // If features is a function, evaluate it directly\n if (typeof featuresSource === \"function\") {\n try {\n const result = featuresSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicFeaturesValue: result };\n } catch (error) {\n documentContext.log.error(\n `Error evaluating features function: ${error}`,\n );\n return { error: `Features function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each feature in the record\n const result: Record<string, MetricValue> = {};\n Object.entries(featuresSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFile(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): string {\n // Ensure the output directory exists\n const fullOutputDir = path.resolve(process.cwd(), this.outputDir);\n fs.mkdirSync(fullOutputDir, { recursive: true });\n\n // Get the file path from the document URI and normalize it\n const filePath = normalizePath(documentContext.document.uri);\n\n // Generate metrics\n const stats: MetricsStats = this.generateMetrics(\n diagnostics,\n documentContext,\n );\n const features: MetricsFeatures = this.generateFeatures(\n diagnostics,\n documentContext,\n );\n\n // Build this file's entry\n const newEntry: MetricsContent = {\n stats,\n ...(Object.keys(features).length > 0 ? { features } : {}),\n };\n\n // Evaluate root properties\n let rootProps: MetricsRoot;\n if (typeof this.rootProperties === \"function\") {\n try {\n const result = this.rootProperties(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n rootProps = result as Record<string, any>;\n } else {\n rootProps = { dynamicRootValue: result };\n }\n } catch (error) {\n documentContext.log.error(`Error evaluating root properties: ${error}`);\n rootProps = { error: `Root properties evaluation failed: ${error}` };\n }\n } else {\n rootProps = this.rootProperties as Record<string, any>;\n }\n\n // Single deep merge of root properties and content for this file\n this.aggregatedResults = merge<MetricsReport>(\n this.aggregatedResults,\n rootProps,\n { content: { [filePath]: newEntry } },\n );\n\n // Write ordered results: all root properties first, then content last\n const outputFilePath = path.join(fullOutputDir, `${this.fileName}.json`);\n const { content, ...root } = this.aggregatedResults;\n fs.writeFileSync(\n outputFilePath,\n JSON.stringify({ ...root, content }, null, 2),\n \"utf-8\",\n );\n\n return outputFilePath;\n }\n}\n","import { Diagnostic } from \"vscode-languageserver-types\";\n\n/**\n * Extracts data from diagnostic messages using a pattern\n */\nexport function extractFromDiagnostics<T>(\n pattern: RegExp,\n parser: (value: string) => T,\n): (diagnostics: Diagnostic[]) => T | undefined {\n return (diagnostics: Diagnostic[]): T | undefined => {\n for (const diagnostic of diagnostics) {\n const match = diagnostic.message.match(pattern);\n if (match && match[1]) {\n try {\n const result = parser(match[1]);\n // Check if result is NaN (only relevant for numeric parsers)\n if (typeof result === \"number\" && isNaN(result)) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`);\n return undefined;\n }\n return result;\n } catch (e) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`, e);\n return undefined;\n }\n }\n }\n return undefined;\n };\n}\n\n/**\n * Extracts data from diagnostics\n */\nexport function extractByData(\n data: string | symbol,\n diagnostics: Diagnostic[],\n parser?: (diagnostic: Diagnostic) => any,\n): Record<string, any> {\n const filteredDiagnostics = diagnostics.filter(\n (diagnostic) => diagnostic.data === data,\n );\n if (filteredDiagnostics.length === 0) {\n return {};\n }\n\n // Default parser that attempts to parse the message as JSON or returns the raw message\n const defaultParser = (diagnostic: Diagnostic): any => {\n try {\n if (diagnostic.message) {\n return JSON.parse(diagnostic.message);\n }\n return diagnostic.message || {};\n } catch (e) {\n return diagnostic.message || {};\n }\n };\n\n const actualParser = parser || defaultParser;\n\n // Collect all information from the specified source\n const result: Record<string, any> = {};\n for (const diagnostic of filteredDiagnostics) {\n try {\n const extractedData = actualParser(diagnostic);\n\n if (typeof extractedData === \"object\" && extractedData !== null) {\n // If object, merge with existing data\n Object.assign(result, extractedData);\n } else {\n // Otherwise store as separate entries\n const key = `entry_${Object.keys(result).length}`;\n result[key] = extractedData;\n }\n } catch (e) {\n console.warn(\n `Failed to process diagnostic from data ${String(data)}:`,\n e,\n );\n }\n }\n\n return result; // Always returns an object, even if empty\n}\n"],"mappings":"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","names":["deepMerge","result"]}
1
+ {"version":3,"sources":["../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/index.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/metrics-output.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/utils.ts"],"sourcesContent":["export * from \"./metrics-output\";\nexport * from \"./utils\";\nexport * from \"./types\";\n","import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { merge as deepMerge } from \"ts-deepmerge\";\nimport { Diagnostic } from \"vscode-languageserver-types\";\nimport type {\n PlayerLanguageService,\n PlayerLanguageServicePlugin,\n DocumentContext,\n} from \"@player-tools/json-language-service\";\nimport type {\n MetricsRoot,\n MetricsStats,\n MetricsFeatures,\n MetricsContent,\n MetricsReport,\n MetricValue,\n} from \"./types\";\n\nexport interface MetricsOutputConfig {\n /** Directory where the output file will be written */\n outputDir?: string;\n\n /** Name of the JSON output file */\n fileName?: string;\n\n /**\n * Custom properties to include at the root level of the output\n */\n rootProperties?: MetricsRoot;\n\n /**\n * Content-specific stats\n */\n stats?: MetricsStats;\n\n /**\n * Content-specific features\n */\n features?: MetricsFeatures;\n}\n\n/**\n * Normalizes a file path to use consistent separators and format\n */\nfunction normalizePath(filePath: string): string {\n // Convert backslashes to forward slashes for consistency\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Remove file:// protocol if present\n return normalized.replace(/^file:\\/\\//, \"\");\n}\n\n// Narrow ts-deepmerge’s generic return type to what's needed\nconst merge = deepMerge as <T>(...objects: Array<Partial<T>>) => T;\n\n/**\n * A plugin that writes diagnostic results to a JSON file in a specified output directory.\n * NOTE: This plugin is designed for CLI usage only and should not be used in an IDE.\n */\nexport class MetricsOutput implements PlayerLanguageServicePlugin {\n name = \"metrics-output-plugin\";\n\n private outputDir: string;\n private fileName: string;\n private rootProperties: MetricsRoot;\n private stats: MetricsStats;\n private features: MetricsFeatures;\n\n // In-memory storage of all results\n private aggregatedResults: MetricsReport = {\n content: {},\n };\n\n private get outputFilePath(): string {\n return path.resolve(this.outputDir, `${this.fileName}.json`);\n }\n\n constructor(options: MetricsOutputConfig = {}) {\n this.outputDir = options.outputDir || process.cwd();\n\n // Handle file name, stripping .json extension if provided\n let fileName = options.fileName || \"metrics\";\n if (fileName.endsWith(\".json\")) {\n fileName = fileName.split(\".\")[0]; // Remove extension\n }\n this.fileName = fileName;\n this.rootProperties = options.rootProperties || {};\n this.stats = options.stats || {};\n this.features = options.features || {};\n }\n\n apply(service: PlayerLanguageService): void {\n // Hook into the validation end to capture diagnostics and write output\n service.hooks.onValidateEnd.tap(\n this.name,\n (\n diagnostics: Diagnostic[],\n { documentContext }: { documentContext: DocumentContext },\n ): Diagnostic[] => {\n // If metrics file exists, load it\n if (fs.existsSync(this.outputFilePath)) {\n this.loadExistingMetrics();\n }\n\n this.generateFile(diagnostics, documentContext);\n\n return diagnostics;\n },\n );\n }\n\n private loadExistingMetrics(): void {\n try {\n const fileContent = fs.readFileSync(this.outputFilePath, \"utf-8\");\n\n // Handle empty file case - treat it as if no file exists\n if (!fileContent.trim()) {\n return;\n }\n\n const parsed: unknown = JSON.parse(fileContent);\n const existingMetrics: Partial<MetricsReport> =\n parsed && typeof parsed === \"object\"\n ? (parsed as Partial<MetricsReport>)\n : {};\n\n // Recursively merge existing metrics with current aggregated results\n this.aggregatedResults = merge<MetricsReport>(\n existingMetrics,\n this.aggregatedResults,\n );\n } catch (error) {\n // If we can't parse existing file, continue with current state\n console.warn(\n `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`,\n );\n }\n }\n\n /**\n * Evaluates a value, executing it if it's a function\n */\n private evaluateValue(\n value: MetricValue,\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ) {\n if (typeof value === \"function\") {\n try {\n return value(diagnostics, documentContext);\n } catch (error) {\n documentContext.log.error(`Error evaluating value: ${error}`);\n return { error: `Value evaluation failed: ${error}` };\n }\n }\n return value;\n }\n\n private generateMetrics(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): MetricsStats {\n const statsSource = this.stats;\n // If stats is a function, evaluate it directly\n if (typeof statsSource === \"function\") {\n try {\n const result = statsSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicStatsValue: result };\n } catch (error) {\n documentContext.log.error(`Error evaluating stats function: ${error}`);\n return { error: `Stats function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each metric in the record\n const result: MetricsStats = {};\n Object.entries(statsSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFeatures(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): Record<string, MetricValue> {\n const featuresSource = this.features;\n // If features is a function, evaluate it directly\n if (typeof featuresSource === \"function\") {\n try {\n const result = featuresSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicFeaturesValue: result };\n } catch (error) {\n documentContext.log.error(\n `Error evaluating features function: ${error}`,\n );\n return { error: `Features function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each feature in the record\n const result: Record<string, MetricValue> = {};\n Object.entries(featuresSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFile(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): string {\n // Ensure the output directory exists\n const fullOutputDir = path.resolve(process.cwd(), this.outputDir);\n fs.mkdirSync(fullOutputDir, { recursive: true });\n\n // Get the file path from the document URI and normalize it\n const filePath = normalizePath(documentContext.document.uri);\n\n // Generate metrics\n const stats: MetricsStats = this.generateMetrics(\n diagnostics,\n documentContext,\n );\n const features: MetricsFeatures = this.generateFeatures(\n diagnostics,\n documentContext,\n );\n\n // Build this file's entry\n const newEntry: MetricsContent = {\n stats,\n ...(Object.keys(features).length > 0 ? { features } : {}),\n };\n\n // Evaluate root properties\n let rootProps: MetricsRoot;\n if (typeof this.rootProperties === \"function\") {\n try {\n const result = this.rootProperties(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n rootProps = result as Record<string, any>;\n } else {\n rootProps = { dynamicRootValue: result };\n }\n } catch (error) {\n documentContext.log.error(`Error evaluating root properties: ${error}`);\n rootProps = { error: `Root properties evaluation failed: ${error}` };\n }\n } else {\n rootProps = this.rootProperties as Record<string, any>;\n }\n\n // Single deep merge of root properties and content for this file\n this.aggregatedResults = merge<MetricsReport>(\n this.aggregatedResults,\n rootProps,\n { content: { [filePath]: newEntry } },\n );\n\n // Write ordered results: all root properties first, then content last\n const outputFilePath = path.join(fullOutputDir, `${this.fileName}.json`);\n const { content, ...root } = this.aggregatedResults;\n fs.writeFileSync(\n outputFilePath,\n JSON.stringify({ ...root, content }, null, 2),\n \"utf-8\",\n );\n\n return outputFilePath;\n }\n}\n","import { Diagnostic } from \"vscode-languageserver-types\";\n\n/**\n * Extracts data from diagnostic messages using a pattern\n */\nexport function extractFromDiagnostics<T>(\n pattern: RegExp,\n parser: (value: string) => T,\n): (diagnostics: Diagnostic[]) => T | undefined {\n return (diagnostics: Diagnostic[]): T | undefined => {\n for (const diagnostic of diagnostics) {\n const match = diagnostic.message.match(pattern);\n if (match && match[1]) {\n try {\n const result = parser(match[1]);\n // Check if result is NaN (only relevant for numeric parsers)\n if (typeof result === \"number\" && isNaN(result)) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`);\n return undefined;\n }\n return result;\n } catch (e) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`, e);\n return undefined;\n }\n }\n }\n return undefined;\n };\n}\n\n/**\n * Extracts data from diagnostics\n */\nexport function extractByData(\n data: string | symbol,\n diagnostics: Diagnostic[],\n parser?: (diagnostic: Diagnostic) => any,\n): Record<string, any> {\n const filteredDiagnostics = diagnostics.filter(\n (diagnostic) => diagnostic.data === data,\n );\n if (filteredDiagnostics.length === 0) {\n return {};\n }\n\n // Default parser that attempts to parse the message as JSON or returns the raw message\n const defaultParser = (diagnostic: Diagnostic): any => {\n try {\n if (diagnostic.message) {\n return JSON.parse(diagnostic.message);\n }\n return diagnostic.message || {};\n } catch (e) {\n return diagnostic.message || {};\n }\n };\n\n const actualParser = parser || defaultParser;\n\n // Collect all information from the specified source\n const result: Record<string, any> = {};\n for (const diagnostic of filteredDiagnostics) {\n try {\n const extractedData = actualParser(diagnostic);\n\n if (typeof extractedData === \"object\" && extractedData !== null) {\n // If object, merge with existing data\n Object.assign(result, extractedData);\n } else {\n // Otherwise store as separate entries\n const key = `entry_${Object.keys(result).length}`;\n result[key] = extractedData;\n }\n } catch (e) {\n console.warn(\n `Failed to process diagnostic from data ${String(data)}:`,\n e,\n );\n }\n }\n\n return result; // Always returns an object, even if empty\n}\n"],"mappings":"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","names":["deepMerge","result"]}
@@ -47,6 +47,9 @@ var MetricsOutput = class {
47
47
  loadExistingMetrics() {
48
48
  try {
49
49
  const fileContent = fs.readFileSync(this.outputFilePath, "utf-8");
50
+ if (!fileContent.trim()) {
51
+ return;
52
+ }
50
53
  const parsed = JSON.parse(fileContent);
51
54
  const existingMetrics = parsed && typeof parsed === "object" ? parsed : {};
52
55
  this.aggregatedResults = merge(
@@ -55,7 +58,7 @@ var MetricsOutput = class {
55
58
  );
56
59
  } catch (error) {
57
60
  console.warn(
58
- `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`
61
+ `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`
59
62
  );
60
63
  }
61
64
  }
package/dist/index.mjs CHANGED
@@ -47,6 +47,9 @@ var MetricsOutput = class {
47
47
  loadExistingMetrics() {
48
48
  try {
49
49
  const fileContent = fs.readFileSync(this.outputFilePath, "utf-8");
50
+ if (!fileContent.trim()) {
51
+ return;
52
+ }
50
53
  const parsed = JSON.parse(fileContent);
51
54
  const existingMetrics = parsed && typeof parsed === "object" ? parsed : {};
52
55
  this.aggregatedResults = merge(
@@ -55,7 +58,7 @@ var MetricsOutput = class {
55
58
  );
56
59
  } catch (error) {
57
60
  console.warn(
58
- `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`
61
+ `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`
59
62
  );
60
63
  }
61
64
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/metrics-output.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/utils.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { merge as deepMerge } from \"ts-deepmerge\";\nimport { Diagnostic } from \"vscode-languageserver-types\";\nimport type {\n PlayerLanguageService,\n PlayerLanguageServicePlugin,\n DocumentContext,\n} from \"@player-tools/json-language-service\";\nimport type {\n MetricsRoot,\n MetricsStats,\n MetricsFeatures,\n MetricsContent,\n MetricsReport,\n MetricValue,\n} from \"./types\";\n\nexport interface MetricsOutputConfig {\n /** Directory where the output file will be written */\n outputDir?: string;\n\n /** Name of the JSON output file */\n fileName?: string;\n\n /**\n * Custom properties to include at the root level of the output\n */\n rootProperties?: MetricsRoot;\n\n /**\n * Content-specific stats\n */\n stats?: MetricsStats;\n\n /**\n * Content-specific features\n */\n features?: MetricsFeatures;\n}\n\n/**\n * Normalizes a file path to use consistent separators and format\n */\nfunction normalizePath(filePath: string): string {\n // Convert backslashes to forward slashes for consistency\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Remove file:// protocol if present\n return normalized.replace(/^file:\\/\\//, \"\");\n}\n\n// Narrow ts-deepmerge’s generic return type to what's needed\nconst merge = deepMerge as <T>(...objects: Array<Partial<T>>) => T;\n\n/**\n * A plugin that writes diagnostic results to a JSON file in a specified output directory.\n * NOTE: This plugin is designed for CLI usage only and should not be used in an IDE.\n */\nexport class MetricsOutput implements PlayerLanguageServicePlugin {\n name = \"metrics-output-plugin\";\n\n private outputDir: string;\n private fileName: string;\n private rootProperties: MetricsRoot;\n private stats: MetricsStats;\n private features: MetricsFeatures;\n\n // In-memory storage of all results\n private aggregatedResults: MetricsReport = {\n content: {},\n };\n\n private get outputFilePath(): string {\n return path.resolve(this.outputDir, `${this.fileName}.json`);\n }\n\n constructor(options: MetricsOutputConfig = {}) {\n this.outputDir = options.outputDir || process.cwd();\n\n // Handle file name, stripping .json extension if provided\n let fileName = options.fileName || \"metrics\";\n if (fileName.endsWith(\".json\")) {\n fileName = fileName.split(\".\")[0]; // Remove extension\n }\n this.fileName = fileName;\n this.rootProperties = options.rootProperties || {};\n this.stats = options.stats || {};\n this.features = options.features || {};\n }\n\n apply(service: PlayerLanguageService): void {\n // Hook into the validation end to capture diagnostics and write output\n service.hooks.onValidateEnd.tap(\n this.name,\n (\n diagnostics: Diagnostic[],\n { documentContext }: { documentContext: DocumentContext },\n ): Diagnostic[] => {\n // If metrics file exists, load and append to it\n if (fs.existsSync(this.outputFilePath)) {\n this.loadExistingMetrics();\n }\n\n this.generateFile(diagnostics, documentContext);\n\n return diagnostics;\n },\n );\n }\n\n private loadExistingMetrics(): void {\n try {\n const fileContent = fs.readFileSync(this.outputFilePath, \"utf-8\");\n const parsed: unknown = JSON.parse(fileContent);\n const existingMetrics: Partial<MetricsReport> =\n parsed && typeof parsed === \"object\"\n ? (parsed as Partial<MetricsReport>)\n : {};\n\n // Recursively merge existing metrics with current aggregated results\n this.aggregatedResults = merge<MetricsReport>(\n existingMetrics,\n this.aggregatedResults,\n );\n } catch (error) {\n // If we can't parse existing file, continue with current state\n console.warn(\n `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`,\n );\n }\n }\n\n /**\n * Evaluates a value, executing it if it's a function\n */\n private evaluateValue(\n value: MetricValue,\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ) {\n if (typeof value === \"function\") {\n try {\n return value(diagnostics, documentContext);\n } catch (error) {\n documentContext.log.error(`Error evaluating value: ${error}`);\n return { error: `Value evaluation failed: ${error}` };\n }\n }\n return value;\n }\n\n private generateMetrics(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): MetricsStats {\n const statsSource = this.stats;\n // If stats is a function, evaluate it directly\n if (typeof statsSource === \"function\") {\n try {\n const result = statsSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicStatsValue: result };\n } catch (error) {\n documentContext.log.error(`Error evaluating stats function: ${error}`);\n return { error: `Stats function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each metric in the record\n const result: MetricsStats = {};\n Object.entries(statsSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFeatures(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): Record<string, MetricValue> {\n const featuresSource = this.features;\n // If features is a function, evaluate it directly\n if (typeof featuresSource === \"function\") {\n try {\n const result = featuresSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicFeaturesValue: result };\n } catch (error) {\n documentContext.log.error(\n `Error evaluating features function: ${error}`,\n );\n return { error: `Features function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each feature in the record\n const result: Record<string, MetricValue> = {};\n Object.entries(featuresSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFile(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): string {\n // Ensure the output directory exists\n const fullOutputDir = path.resolve(process.cwd(), this.outputDir);\n fs.mkdirSync(fullOutputDir, { recursive: true });\n\n // Get the file path from the document URI and normalize it\n const filePath = normalizePath(documentContext.document.uri);\n\n // Generate metrics\n const stats: MetricsStats = this.generateMetrics(\n diagnostics,\n documentContext,\n );\n const features: MetricsFeatures = this.generateFeatures(\n diagnostics,\n documentContext,\n );\n\n // Build this file's entry\n const newEntry: MetricsContent = {\n stats,\n ...(Object.keys(features).length > 0 ? { features } : {}),\n };\n\n // Evaluate root properties\n let rootProps: MetricsRoot;\n if (typeof this.rootProperties === \"function\") {\n try {\n const result = this.rootProperties(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n rootProps = result as Record<string, any>;\n } else {\n rootProps = { dynamicRootValue: result };\n }\n } catch (error) {\n documentContext.log.error(`Error evaluating root properties: ${error}`);\n rootProps = { error: `Root properties evaluation failed: ${error}` };\n }\n } else {\n rootProps = this.rootProperties as Record<string, any>;\n }\n\n // Single deep merge of root properties and content for this file\n this.aggregatedResults = merge<MetricsReport>(\n this.aggregatedResults,\n rootProps,\n { content: { [filePath]: newEntry } },\n );\n\n // Write ordered results: all root properties first, then content last\n const outputFilePath = path.join(fullOutputDir, `${this.fileName}.json`);\n const { content, ...root } = this.aggregatedResults;\n fs.writeFileSync(\n outputFilePath,\n JSON.stringify({ ...root, content }, null, 2),\n \"utf-8\",\n );\n\n return outputFilePath;\n }\n}\n","import { Diagnostic } from \"vscode-languageserver-types\";\n\n/**\n * Extracts data from diagnostic messages using a pattern\n */\nexport function extractFromDiagnostics<T>(\n pattern: RegExp,\n parser: (value: string) => T,\n): (diagnostics: Diagnostic[]) => T | undefined {\n return (diagnostics: Diagnostic[]): T | undefined => {\n for (const diagnostic of diagnostics) {\n const match = diagnostic.message.match(pattern);\n if (match && match[1]) {\n try {\n const result = parser(match[1]);\n // Check if result is NaN (only relevant for numeric parsers)\n if (typeof result === \"number\" && isNaN(result)) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`);\n return undefined;\n }\n return result;\n } catch (e) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`, e);\n return undefined;\n }\n }\n }\n return undefined;\n };\n}\n\n/**\n * Extracts data from diagnostics\n */\nexport function extractByData(\n data: string | symbol,\n diagnostics: Diagnostic[],\n parser?: (diagnostic: Diagnostic) => any,\n): Record<string, any> {\n const filteredDiagnostics = diagnostics.filter(\n (diagnostic) => diagnostic.data === data,\n );\n if (filteredDiagnostics.length === 0) {\n return {};\n }\n\n // Default parser that attempts to parse the message as JSON or returns the raw message\n const defaultParser = (diagnostic: Diagnostic): any => {\n try {\n if (diagnostic.message) {\n return JSON.parse(diagnostic.message);\n }\n return diagnostic.message || {};\n } catch (e) {\n return diagnostic.message || {};\n }\n };\n\n const actualParser = parser || defaultParser;\n\n // Collect all information from the specified source\n const result: Record<string, any> = {};\n for (const diagnostic of filteredDiagnostics) {\n try {\n const extractedData = actualParser(diagnostic);\n\n if (typeof extractedData === \"object\" && extractedData !== null) {\n // If object, merge with existing data\n Object.assign(result, extractedData);\n } else {\n // Otherwise store as separate entries\n const key = `entry_${Object.keys(result).length}`;\n result[key] = extractedData;\n }\n } catch (e) {\n console.warn(\n `Failed to process diagnostic from data ${String(data)}:`,\n e,\n );\n }\n }\n\n return result; // Always returns an object, even if empty\n}\n"],"mappings":"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","names":["result"]}
1
+ {"version":3,"sources":["../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/metrics-output.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/language/metrics-output-plugin/src/utils.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { merge as deepMerge } from \"ts-deepmerge\";\nimport { Diagnostic } from \"vscode-languageserver-types\";\nimport type {\n PlayerLanguageService,\n PlayerLanguageServicePlugin,\n DocumentContext,\n} from \"@player-tools/json-language-service\";\nimport type {\n MetricsRoot,\n MetricsStats,\n MetricsFeatures,\n MetricsContent,\n MetricsReport,\n MetricValue,\n} from \"./types\";\n\nexport interface MetricsOutputConfig {\n /** Directory where the output file will be written */\n outputDir?: string;\n\n /** Name of the JSON output file */\n fileName?: string;\n\n /**\n * Custom properties to include at the root level of the output\n */\n rootProperties?: MetricsRoot;\n\n /**\n * Content-specific stats\n */\n stats?: MetricsStats;\n\n /**\n * Content-specific features\n */\n features?: MetricsFeatures;\n}\n\n/**\n * Normalizes a file path to use consistent separators and format\n */\nfunction normalizePath(filePath: string): string {\n // Convert backslashes to forward slashes for consistency\n const normalized = filePath.replace(/\\\\/g, \"/\");\n\n // Remove file:// protocol if present\n return normalized.replace(/^file:\\/\\//, \"\");\n}\n\n// Narrow ts-deepmerge’s generic return type to what's needed\nconst merge = deepMerge as <T>(...objects: Array<Partial<T>>) => T;\n\n/**\n * A plugin that writes diagnostic results to a JSON file in a specified output directory.\n * NOTE: This plugin is designed for CLI usage only and should not be used in an IDE.\n */\nexport class MetricsOutput implements PlayerLanguageServicePlugin {\n name = \"metrics-output-plugin\";\n\n private outputDir: string;\n private fileName: string;\n private rootProperties: MetricsRoot;\n private stats: MetricsStats;\n private features: MetricsFeatures;\n\n // In-memory storage of all results\n private aggregatedResults: MetricsReport = {\n content: {},\n };\n\n private get outputFilePath(): string {\n return path.resolve(this.outputDir, `${this.fileName}.json`);\n }\n\n constructor(options: MetricsOutputConfig = {}) {\n this.outputDir = options.outputDir || process.cwd();\n\n // Handle file name, stripping .json extension if provided\n let fileName = options.fileName || \"metrics\";\n if (fileName.endsWith(\".json\")) {\n fileName = fileName.split(\".\")[0]; // Remove extension\n }\n this.fileName = fileName;\n this.rootProperties = options.rootProperties || {};\n this.stats = options.stats || {};\n this.features = options.features || {};\n }\n\n apply(service: PlayerLanguageService): void {\n // Hook into the validation end to capture diagnostics and write output\n service.hooks.onValidateEnd.tap(\n this.name,\n (\n diagnostics: Diagnostic[],\n { documentContext }: { documentContext: DocumentContext },\n ): Diagnostic[] => {\n // If metrics file exists, load it\n if (fs.existsSync(this.outputFilePath)) {\n this.loadExistingMetrics();\n }\n\n this.generateFile(diagnostics, documentContext);\n\n return diagnostics;\n },\n );\n }\n\n private loadExistingMetrics(): void {\n try {\n const fileContent = fs.readFileSync(this.outputFilePath, \"utf-8\");\n\n // Handle empty file case - treat it as if no file exists\n if (!fileContent.trim()) {\n return;\n }\n\n const parsed: unknown = JSON.parse(fileContent);\n const existingMetrics: Partial<MetricsReport> =\n parsed && typeof parsed === \"object\"\n ? (parsed as Partial<MetricsReport>)\n : {};\n\n // Recursively merge existing metrics with current aggregated results\n this.aggregatedResults = merge<MetricsReport>(\n existingMetrics,\n this.aggregatedResults,\n );\n } catch (error) {\n // If we can't parse existing file, continue with current state\n console.warn(\n `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`,\n );\n }\n }\n\n /**\n * Evaluates a value, executing it if it's a function\n */\n private evaluateValue(\n value: MetricValue,\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ) {\n if (typeof value === \"function\") {\n try {\n return value(diagnostics, documentContext);\n } catch (error) {\n documentContext.log.error(`Error evaluating value: ${error}`);\n return { error: `Value evaluation failed: ${error}` };\n }\n }\n return value;\n }\n\n private generateMetrics(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): MetricsStats {\n const statsSource = this.stats;\n // If stats is a function, evaluate it directly\n if (typeof statsSource === \"function\") {\n try {\n const result = statsSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicStatsValue: result };\n } catch (error) {\n documentContext.log.error(`Error evaluating stats function: ${error}`);\n return { error: `Stats function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each metric in the record\n const result: MetricsStats = {};\n Object.entries(statsSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFeatures(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): Record<string, MetricValue> {\n const featuresSource = this.features;\n // If features is a function, evaluate it directly\n if (typeof featuresSource === \"function\") {\n try {\n const result = featuresSource(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n return result;\n }\n return { dynamicFeaturesValue: result };\n } catch (error) {\n documentContext.log.error(\n `Error evaluating features function: ${error}`,\n );\n return { error: `Features function evaluation failed: ${error}` };\n }\n }\n\n // Otherwise process each feature in the record\n const result: Record<string, MetricValue> = {};\n Object.entries(featuresSource).forEach(([key, value]) => {\n result[key] = this.evaluateValue(value, diagnostics, documentContext);\n });\n\n return result;\n }\n\n private generateFile(\n diagnostics: Diagnostic[],\n documentContext: DocumentContext,\n ): string {\n // Ensure the output directory exists\n const fullOutputDir = path.resolve(process.cwd(), this.outputDir);\n fs.mkdirSync(fullOutputDir, { recursive: true });\n\n // Get the file path from the document URI and normalize it\n const filePath = normalizePath(documentContext.document.uri);\n\n // Generate metrics\n const stats: MetricsStats = this.generateMetrics(\n diagnostics,\n documentContext,\n );\n const features: MetricsFeatures = this.generateFeatures(\n diagnostics,\n documentContext,\n );\n\n // Build this file's entry\n const newEntry: MetricsContent = {\n stats,\n ...(Object.keys(features).length > 0 ? { features } : {}),\n };\n\n // Evaluate root properties\n let rootProps: MetricsRoot;\n if (typeof this.rootProperties === \"function\") {\n try {\n const result = this.rootProperties(diagnostics, documentContext);\n if (typeof result === \"object\" && result !== null) {\n rootProps = result as Record<string, any>;\n } else {\n rootProps = { dynamicRootValue: result };\n }\n } catch (error) {\n documentContext.log.error(`Error evaluating root properties: ${error}`);\n rootProps = { error: `Root properties evaluation failed: ${error}` };\n }\n } else {\n rootProps = this.rootProperties as Record<string, any>;\n }\n\n // Single deep merge of root properties and content for this file\n this.aggregatedResults = merge<MetricsReport>(\n this.aggregatedResults,\n rootProps,\n { content: { [filePath]: newEntry } },\n );\n\n // Write ordered results: all root properties first, then content last\n const outputFilePath = path.join(fullOutputDir, `${this.fileName}.json`);\n const { content, ...root } = this.aggregatedResults;\n fs.writeFileSync(\n outputFilePath,\n JSON.stringify({ ...root, content }, null, 2),\n \"utf-8\",\n );\n\n return outputFilePath;\n }\n}\n","import { Diagnostic } from \"vscode-languageserver-types\";\n\n/**\n * Extracts data from diagnostic messages using a pattern\n */\nexport function extractFromDiagnostics<T>(\n pattern: RegExp,\n parser: (value: string) => T,\n): (diagnostics: Diagnostic[]) => T | undefined {\n return (diagnostics: Diagnostic[]): T | undefined => {\n for (const diagnostic of diagnostics) {\n const match = diagnostic.message.match(pattern);\n if (match && match[1]) {\n try {\n const result = parser(match[1]);\n // Check if result is NaN (only relevant for numeric parsers)\n if (typeof result === \"number\" && isNaN(result)) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`);\n return undefined;\n }\n return result;\n } catch (e) {\n console.warn(`Failed to parse diagnostic value: ${match[1]}`, e);\n return undefined;\n }\n }\n }\n return undefined;\n };\n}\n\n/**\n * Extracts data from diagnostics\n */\nexport function extractByData(\n data: string | symbol,\n diagnostics: Diagnostic[],\n parser?: (diagnostic: Diagnostic) => any,\n): Record<string, any> {\n const filteredDiagnostics = diagnostics.filter(\n (diagnostic) => diagnostic.data === data,\n );\n if (filteredDiagnostics.length === 0) {\n return {};\n }\n\n // Default parser that attempts to parse the message as JSON or returns the raw message\n const defaultParser = (diagnostic: Diagnostic): any => {\n try {\n if (diagnostic.message) {\n return JSON.parse(diagnostic.message);\n }\n return diagnostic.message || {};\n } catch (e) {\n return diagnostic.message || {};\n }\n };\n\n const actualParser = parser || defaultParser;\n\n // Collect all information from the specified source\n const result: Record<string, any> = {};\n for (const diagnostic of filteredDiagnostics) {\n try {\n const extractedData = actualParser(diagnostic);\n\n if (typeof extractedData === \"object\" && extractedData !== null) {\n // If object, merge with existing data\n Object.assign(result, extractedData);\n } else {\n // Otherwise store as separate entries\n const key = `entry_${Object.keys(result).length}`;\n result[key] = extractedData;\n }\n } catch (e) {\n console.warn(\n `Failed to process diagnostic from data ${String(data)}:`,\n e,\n );\n }\n }\n\n return result; // Always returns an object, even if empty\n}\n"],"mappings":"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","names":["result"]}
package/package.json CHANGED
@@ -6,10 +6,10 @@
6
6
  "types"
7
7
  ],
8
8
  "name": "@player-tools/metrics-output-plugin",
9
- "version": "0.13.0--canary.231.5583",
9
+ "version": "0.13.0--canary.231.5678",
10
10
  "main": "dist/cjs/index.cjs",
11
11
  "dependencies": {
12
- "@player-tools/json-language-service": "0.13.0--canary.231.5583",
12
+ "@player-tools/json-language-service": "0.13.0--canary.231.5678",
13
13
  "@player-ui/player": "0.12.0-next.1",
14
14
  "jsonc-parser": "^2.3.1",
15
15
  "typescript-template-language-service-decorator": "^2.3.1",
@@ -1340,6 +1340,40 @@ describe("WriteMetricsPlugin", () => {
1340
1340
  warnSpy.mockRestore();
1341
1341
  });
1342
1342
 
1343
+ test("Gracefully handles empty existing metrics file without warning", async () => {
1344
+ // Seed an empty metrics file to test the empty file path
1345
+ fs.writeFileSync(MULTI_TEST_PATH, "");
1346
+
1347
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
1348
+
1349
+ const service = new PlayerLanguageService();
1350
+ service.addLSPPlugin(
1351
+ new MetricsOutput({
1352
+ outputDir: MULTI_TEST_DIR,
1353
+ fileName: MULTI_TEST_FILE.replace(".json", ""),
1354
+ stats: { metric: () => 42 },
1355
+ }),
1356
+ );
1357
+
1358
+ await service.setAssetTypesFromModule([
1359
+ Types,
1360
+ ReferenceAssetsWebPluginManifest,
1361
+ ]);
1362
+
1363
+ const doc = TextDocument.create("file:///empty.json", "json", 1, "{}");
1364
+ await service.validateTextDocument(doc);
1365
+
1366
+ // Should NOT log any warnings for empty files
1367
+ expect(warnSpy).not.toHaveBeenCalled();
1368
+
1369
+ // Should still produce a valid metrics file
1370
+ const parsed = JSON.parse(fs.readFileSync(MULTI_TEST_PATH, "utf-8"));
1371
+ expect(parsed.content).toHaveProperty("/empty.json");
1372
+ expect(parsed.content["/empty.json"].stats.metric).toBe(42);
1373
+
1374
+ warnSpy.mockRestore();
1375
+ });
1376
+
1343
1377
  test("loads and merges an existing metrics file", async () => {
1344
1378
  const service = new PlayerLanguageService();
1345
1379
  const fileName = "pre_merge";
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./metrics-output";
2
2
  export * from "./utils";
3
+ export * from "./types";
@@ -97,7 +97,7 @@ export class MetricsOutput implements PlayerLanguageServicePlugin {
97
97
  diagnostics: Diagnostic[],
98
98
  { documentContext }: { documentContext: DocumentContext },
99
99
  ): Diagnostic[] => {
100
- // If metrics file exists, load and append to it
100
+ // If metrics file exists, load it
101
101
  if (fs.existsSync(this.outputFilePath)) {
102
102
  this.loadExistingMetrics();
103
103
  }
@@ -112,6 +112,12 @@ export class MetricsOutput implements PlayerLanguageServicePlugin {
112
112
  private loadExistingMetrics(): void {
113
113
  try {
114
114
  const fileContent = fs.readFileSync(this.outputFilePath, "utf-8");
115
+
116
+ // Handle empty file case - treat it as if no file exists
117
+ if (!fileContent.trim()) {
118
+ return;
119
+ }
120
+
115
121
  const parsed: unknown = JSON.parse(fileContent);
116
122
  const existingMetrics: Partial<MetricsReport> =
117
123
  parsed && typeof parsed === "object"
@@ -126,7 +132,7 @@ export class MetricsOutput implements PlayerLanguageServicePlugin {
126
132
  } catch (error) {
127
133
  // If we can't parse existing file, continue with current state
128
134
  console.warn(
129
- `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics.`,
135
+ `Warning: Could not parse existing metrics file ${this.outputFilePath}. Continuing with current metrics. Error: ${error}`,
130
136
  );
131
137
  }
132
138
  }
package/src/types.ts CHANGED
@@ -19,3 +19,8 @@ export type MetricsContent = {
19
19
  export type MetricsReport = MetricsRoot & {
20
20
  content: Record<string, MetricsContent>;
21
21
  };
22
+
23
+ export type MetricItem = MetricsContent & {
24
+ file: string;
25
+ path?: string;
26
+ };
package/types/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./metrics-output";
2
2
  export * from "./utils";
3
+ export * from "./types";
3
4
  //# sourceMappingURL=index.d.ts.map
package/types/types.d.ts CHANGED
@@ -10,4 +10,8 @@ export type MetricsContent = {
10
10
  export type MetricsReport = MetricsRoot & {
11
11
  content: Record<string, MetricsContent>;
12
12
  };
13
+ export type MetricItem = MetricsContent & {
14
+ file: string;
15
+ path?: string;
16
+ };
13
17
  //# sourceMappingURL=types.d.ts.map