@warlock.js/core 5.16.0 → 5.17.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/CHANGELOG.md +30 -0
- package/esm/cli/commands/migrate.command.mjs +5 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs +2 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +16 -23
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +7 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +1 -18
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/read-bound-port.mjs +32 -0
- package/esm/connectors/read-bound-port.mjs.map +1 -0
- package/esm/dev-server/development-server.mjs +6 -2
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/extract-translation-keys.mjs +68 -0
- package/esm/dev-server/extract-translation-keys.mjs.map +1 -0
- package/esm/dev-server/health-checker/checkers/typescript-health-checker.mjs +5 -1
- package/esm/dev-server/health-checker/checkers/typescript-health-checker.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/ts-health.worker.mjs +37 -8
- package/esm/dev-server/health-checker/workers/ts-health.worker.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +19 -0
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +64 -11
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/stubs.mjs +7 -6
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/csrf-default-guard.d.mts +32 -0
- package/esm/http/csrf-default-guard.d.mts.map +1 -0
- package/esm/http/csrf-default-guard.mjs +156 -0
- package/esm/http/csrf-default-guard.mjs.map +1 -0
- package/esm/http/csrf-origin-policy.d.mts +53 -0
- package/esm/http/csrf-origin-policy.d.mts.map +1 -0
- package/esm/http/csrf-origin-policy.mjs +104 -0
- package/esm/http/csrf-origin-policy.mjs.map +1 -0
- package/esm/http/error-codes.d.mts +14 -1
- package/esm/http/error-codes.d.mts.map +1 -1
- package/esm/http/error-codes.mjs +13 -0
- package/esm/http/error-codes.mjs.map +1 -1
- package/esm/http/index.d.mts +7 -1
- package/esm/http/index.mjs +7 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +6 -0
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/request.d.mts +2 -2
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +7 -3
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +3 -3
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +7 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/uploads/detect-image-format.mjs +59 -0
- package/esm/http/uploads/detect-image-format.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variant.mjs +46 -0
- package/esm/http/uploads/generate-image-variant.mjs.map +1 -0
- package/esm/http/uploads/generate-image-variants.d.mts +49 -0
- package/esm/http/uploads/generate-image-variants.d.mts.map +1 -0
- package/esm/http/uploads/generate-image-variants.mjs +141 -0
- package/esm/http/uploads/generate-image-variants.mjs.map +1 -0
- package/esm/http/uploads/image-variant-types.d.mts +93 -0
- package/esm/http/uploads/image-variant-types.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.d.mts +13 -0
- package/esm/http/uploads/image-variants-config-error.d.mts.map +1 -0
- package/esm/http/uploads/image-variants-config-error.mjs +17 -0
- package/esm/http/uploads/image-variants-config-error.mjs.map +1 -0
- package/esm/http/uploads/index.d.mts +4 -0
- package/esm/http/uploads/index.mjs +5 -0
- package/esm/http/uploads/is-path-inside.mjs +26 -0
- package/esm/http/uploads/is-path-inside.mjs.map +1 -0
- package/esm/http/uploads/load-variant-source.mjs +42 -0
- package/esm/http/uploads/load-variant-source.mjs.map +1 -0
- package/esm/http/uploads/matches-if-none-match.mjs +13 -0
- package/esm/http/uploads/matches-if-none-match.mjs.map +1 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs +51 -0
- package/esm/http/uploads/parse-uploaded-file-query.mjs.map +1 -0
- package/esm/http/uploads/read-file-head.mjs +20 -0
- package/esm/http/uploads/read-file-head.mjs.map +1 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs +91 -0
- package/esm/http/uploads/resolve-image-variants-config.mjs.map +1 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs +53 -0
- package/esm/http/uploads/resolve-inline-image-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-original-content-type.mjs +31 -0
- package/esm/http/uploads/resolve-original-content-type.mjs.map +1 -0
- package/esm/http/uploads/resolve-upload-path.mjs +56 -0
- package/esm/http/uploads/resolve-upload-path.mjs.map +1 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs +39 -0
- package/esm/http/uploads/resolve-variant-candidate.mjs.map +1 -0
- package/esm/http/uploads/single-flight.mjs +40 -0
- package/esm/http/uploads/single-flight.mjs.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts +24 -0
- package/esm/http/uploads/uploaded-file.controller.d.mts.map +1 -0
- package/esm/http/uploads/uploaded-file.controller.mjs +174 -0
- package/esm/http/uploads/uploaded-file.controller.mjs.map +1 -0
- package/esm/http/uploads/variant-cache-key.mjs +45 -0
- package/esm/http/uploads/variant-cache-key.mjs.map +1 -0
- package/esm/http/uploads/variant-generations.mjs +16 -0
- package/esm/http/uploads/variant-generations.mjs.map +1 -0
- package/esm/http/uploads-types.d.mts +102 -1
- package/esm/http/uploads-types.d.mts.map +1 -1
- package/esm/index.d.mts +9 -2
- package/esm/index.mjs +8 -2
- package/esm/router/types.d.mts +23 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/index.d.mts +1 -0
- package/esm/storage/index.mjs +1 -0
- package/esm/storage/scoped-storage.d.mts +1 -0
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +3 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +2 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +4 -0
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/storage-not-initialized-error.d.mts +22 -0
- package/esm/storage/utils/storage-not-initialized-error.d.mts.map +1 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs +24 -0
- package/esm/storage/utils/storage-not-initialized-error.mjs.map +1 -0
- package/llms-full.txt +17 -1
- package/package.json +22 -21
- package/skills/use-localization/SKILL.md +16 -0
- package/skills/use-middleware/SKILL.md +1 -1
- package/esm/dev-server/translation-type-generator.mjs +0 -28
- package/esm/dev-server/translation-type-generator.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript-health-checker.mjs","names":[],"sources":["../../../../../../../../../core/src/dev-server/health-checker/checkers/typescript-health-checker.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport ts from \"typescript\";\nimport type { FileManager } from \"../../file-manager\";\nimport { tsconfigManager } from \"../../tsconfig-manager\";\nimport type { FileHealthCheckerContract } from \"../file-health-checker.contract\";\nimport { FileHealthResult } from \"../file-health-result\";\nimport { BaseHealthChecker } from \"./base-health-checker\";\n\nexport class TypescriptHealthChecker\n extends BaseHealthChecker\n implements FileHealthCheckerContract\n{\n /**\n * Cached TypeScript program instance\n */\n private program: ts.Program | null = null;\n\n /**\n * Cached parsed TypeScript configuration\n */\n private parsedConfig: ts.ParsedCommandLine | null = null;\n\n /**\n * Health checker name\n */\n public name: string = \"TypeScript\";\n\n /**\n * Path to dedicated worker file for TypeScript checking\n * Runs in a separate thread to avoid blocking the main dev server\n */\n public workerPath: string = \"./workers/ts-health.worker\";\n\n /**\n * Whether checker is initialized\n */\n private initialized: boolean = false;\n\n /**\n * Check if file is a TypeScript file\n */\n private isTypeScriptFile(filePath: string): boolean {\n const ext = filePath.toLowerCase();\n return ext.endsWith(\".ts\") || ext.endsWith(\".tsx\");\n }\n\n /**\n * Extract line and column from diagnostic location\n */\n private getDiagnosticLocation(diagnostic: ts.Diagnostic): {\n lineNumber: number;\n columnNumber: number;\n } {\n if (diagnostic.file && diagnostic.start !== undefined) {\n const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n return {\n lineNumber: line + 1, // TypeScript uses 0-based, we use 1-based\n columnNumber: character + 1,\n };\n }\n return {\n lineNumber: 1,\n columnNumber: 1,\n };\n }\n\n /**\n * Format diagnostic message\n */\n private formatDiagnosticMessage(diagnostic: ts.Diagnostic): string {\n if (diagnostic.file && diagnostic.start !== undefined) {\n return ts.formatDiagnostic(diagnostic, {\n getCurrentDirectory: () => process.cwd(),\n getCanonicalFileName: (fileName) => fileName,\n getNewLine: () => \"\\n\",\n });\n }\n return diagnostic.messageText.toString();\n }\n\n /**\n * Display health check results in a pretty format\n */\n private displayResults(file: FileManager, result: FileHealthResult): void {\n const stats = result.getStats();\n\n // Only display if there are errors or warnings\n if (stats.errors === 0 && stats.warnings === 0) {\n return;\n }\n\n const fileName = file.relativePath.replace(/\\\\/g, \"/\");\n const errorCount = stats.errors;\n const warningCount = stats.warnings;\n const sourceLines = file.source ? file.source.split(\"\\n\") : [];\n\n // Display header\n // console.log(\n // `\\n${colors.dim(\"╭─\")} ${colors.bold(colors.cyanBright(\"TypeScript Health\"))} ${colors.dim(\"→\")} ${colors.cyan(fileName)}`,\n // );\n\n // Display errors\n if (errorCount > 0) {\n const errorMessages = result.messages.filter((m) => m.type === \"error\");\n for (const error of errorMessages) {\n const icon = colors.redBright(\"✖\");\n const level = colors.redBright(colors.bold(\"ERROR\"));\n console.log(\n `\\n${icon} ${level} ${colors.dim(\"in\")} ${colors.cyanBright(fileName)}${colors.dim(`(${error.lineNumber},${error.columnNumber})`)}`,\n );\n // Extract just the message text (remove file path prefix if present)\n const messageLines = error.message.split(\"\\n\");\n const cleanMessage = messageLines\n .map((line) => line.replace(/^[^:]+:\\d+:\\d+ - /, \"\").trim())\n .filter((line) => line.length > 0)\n .join(\"\\n\");\n console.log(` ${colors.dim(\"→\")} ${colors.red(cleanMessage)}`);\n\n // Display code line with underline indicator\n if (\n sourceLines.length > 0 &&\n error.lineNumber > 0 &&\n error.lineNumber <= sourceLines.length\n ) {\n const lineIndex = error.lineNumber - 1; // Convert to 0-based index\n const lineContent = sourceLines[lineIndex];\n const lineNum = error.lineNumber.toString().padStart(4, \" \");\n const errorLength = error.length || 1;\n const columnIndex = error.columnNumber - 1; // Convert to 0-based index\n\n // Display the line with normal color (not all red)\n console.log(` ${colors.dim(lineNum)} ${colors.dim(\"│\")} ${lineContent || \"\"}`);\n\n // Display underline indicator\n // Padding accounts for: lineNum.length + 1 space + 1 (│) + 1 space = lineNum.length + 3\n // (The initial 2 spaces indent is already in the console.log)\n const prefixPadding = lineNum.length + 3;\n const columnPadding = \" \".repeat(columnIndex);\n const underline = colors.redBright(\"~\".repeat(Math.max(1, errorLength)));\n console.log(` ${colors.dim(\" \".repeat(prefixPadding))}${columnPadding}${underline}`);\n }\n }\n }\n\n // Display warnings\n if (warningCount > 0) {\n const warningMessages = result.messages.filter((m) => m.type === \"warning\");\n for (const warning of warningMessages) {\n const icon = colors.yellowBright(\"⚠\");\n const level = colors.yellowBright(colors.bold(\"WARNING\"));\n console.log(\n `\\n${icon} ${level} ${colors.dim(\"in\")} ${colors.cyanBright(fileName)}${colors.dim(`(${warning.lineNumber},${warning.columnNumber})`)}`,\n );\n // Extract just the message text (remove file path prefix if present)\n const messageLines = warning.message.split(\"\\n\");\n const cleanMessage = messageLines\n .map((line) => line.replace(/^[^:]+:\\d+:\\d+ - /, \"\").trim())\n .filter((line) => line.length > 0)\n .join(\"\\n\");\n console.log(` ${colors.dim(\"→\")} ${colors.yellow(cleanMessage)}`);\n\n // Display code line with underline indicator\n if (\n sourceLines.length > 0 &&\n warning.lineNumber > 0 &&\n warning.lineNumber <= sourceLines.length\n ) {\n const lineIndex = warning.lineNumber - 1; // Convert to 0-based index\n const lineContent = sourceLines[lineIndex];\n const lineNum = warning.lineNumber.toString().padStart(4, \" \");\n const warningLength = warning.length || 1;\n const columnIndex = warning.columnNumber - 1; // Convert to 0-based index\n\n // Display the line with normal color (not all yellow)\n console.log(` ${colors.dim(lineNum)} ${colors.dim(\"│\")} ${lineContent || \"\"}`);\n\n // Display underline indicator\n // Padding accounts for: lineNum.length + 1 space + 1 (│) + 1 space = lineNum.length + 3\n // (The initial 2 spaces indent is already in the console.log)\n const prefixPadding = lineNum.length + 3;\n const columnPadding = \" \".repeat(columnIndex);\n const underline = colors.yellowBright(\"~\".repeat(Math.max(1, warningLength)));\n console.log(` ${colors.dim(\" \".repeat(prefixPadding))}${columnPadding}${underline}`);\n }\n }\n }\n\n // Display summary\n const summary = [];\n if (errorCount > 0) {\n summary.push(colors.red(`${errorCount} error${errorCount > 1 ? \"s\" : \"\"}`));\n }\n if (warningCount > 0) {\n summary.push(colors.yellow(`${warningCount} warning${warningCount > 1 ? \"s\" : \"\"}`));\n }\n\n // console.log(\n // `\\n${colors.dim(\"╰─\")} ${colors.bold(\"TypeScript\")} ${colors.dim(\"→\")} ${summary.join(colors.dim(\" and \"))}`,\n // );\n }\n\n /**\n * Detect when files are changed\n */\n public async onFileChanges(files: FileManager[]): Promise<void> {\n // recreate an incremental program\n if (!this.parsedConfig) {\n return;\n }\n\n this.program = ts.createProgram(\n files.map((file) => file.absolutePath),\n {\n ...this.parsedConfig.options,\n incremental: true,\n },\n undefined,\n this.program!,\n );\n }\n\n /**\n * Initialize the health checker\n */\n public initialize(): TypescriptHealthChecker {\n try {\n // Verify tsconfigManager is initialized and has config\n if (!tsconfigManager.tsconfig || Object.keys(tsconfigManager.tsconfig).length === 0) {\n this.initialized = true;\n return this;\n }\n\n // Parse the config using tsconfigManager's cached config\n this.parsedConfig = ts.parseJsonConfigFileContent(\n tsconfigManager.tsconfig,\n ts.sys,\n process.cwd(),\n );\n\n // Check for config errors\n if (this.parsedConfig.errors.length > 0) {\n // Log config errors but continue (will skip validation)\n console.warn(\n \"TypeScript Health Checker: tsconfig.json has errors:\",\n this.parsedConfig.errors.map((e) => ts.formatDiagnostic(e, ts.createCompilerHost({}))),\n );\n }\n\n this.program = ts.createProgram(this.parsedConfig.fileNames, this.parsedConfig.options);\n\n this.initialized = true;\n } catch (error) {\n // Handle any errors during initialization gracefully\n console.warn(\"TypeScript Health Checker: Failed to initialize:\", error);\n this.initialized = true; // Mark as initialized to prevent retries\n }\n\n return this;\n }\n\n /**\n * Validate the health of the file\n */\n public async validate(file: FileManager, result: FileHealthResult): Promise<FileHealthResult> {\n // Early exit: skip non-TypeScript files\n if (!this.isTypeScriptFile(file.absolutePath)) {\n result.markAsHealthy();\n return result;\n }\n\n // Early exit: check if parsed config is available\n if (!this.parsedConfig) {\n result.markAsHealthy();\n return result;\n }\n\n try {\n // Lazy-load program on first validation if not already created\n if (!this.program) {\n // Create program with all project files from parsed config (for proper type checking)\n this.program = ts.createProgram(this.parsedConfig.fileNames, this.parsedConfig.options);\n }\n\n // Get source file from program\n const sourceFile = this.program.getSourceFile(file.absolutePath);\n\n // If file is not in the program, return healthy (might be excluded)\n if (!sourceFile) {\n result.markAsHealthy();\n return result;\n }\n\n // Get diagnostics for the specific file only\n const syntacticDiagnostics = this.program.getSyntacticDiagnostics(sourceFile);\n const semanticDiagnostics = this.program.getSemanticDiagnostics(sourceFile);\n\n // Combine all diagnostics\n const allDiagnostics = [...syntacticDiagnostics, ...semanticDiagnostics];\n\n // Convert TypeScript diagnostics to FileHealthResult format\n const errors: Array<{\n message: string;\n type: \"error\";\n lineNumber: number;\n columnNumber: number;\n length: number;\n }> = [];\n const warnings: Array<{\n message: string;\n type: \"warning\";\n lineNumber: number;\n columnNumber: number;\n length: number;\n }> = [];\n\n for (const diagnostic of allDiagnostics) {\n const location = this.getDiagnosticLocation(diagnostic);\n const message = this.formatDiagnosticMessage(diagnostic);\n const errorLength = diagnostic.length || 1;\n\n if (diagnostic.category === ts.DiagnosticCategory.Error) {\n errors.push({\n message,\n type: \"error\",\n lineNumber: location.lineNumber,\n columnNumber: location.columnNumber,\n length: errorLength,\n });\n } else if (diagnostic.category === ts.DiagnosticCategory.Warning) {\n warnings.push({\n message,\n type: \"warning\",\n lineNumber: location.lineNumber,\n columnNumber: location.columnNumber,\n length: errorLength,\n });\n }\n }\n\n // Add errors and warnings to result\n if (errors.length > 0) {\n result.addErrors(errors);\n }\n if (warnings.length > 0) {\n result.addWarnings(warnings);\n }\n\n // If no errors or warnings, mark as healthy\n if (errors.length === 0 && warnings.length === 0) {\n result.markAsHealthy();\n } else {\n // Display results if there are errors or warnings\n this.displayResults(file, result);\n }\n } catch (error) {\n // Handle any errors during validation gracefully\n console.warn(`TypeScript Health Checker: Error validating file ${file.relativePath}:`, error);\n result.markAsHealthy(); // Return healthy on error to avoid blocking\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"typescript-health-checker.mjs","names":[],"sources":["../../../../../../../../../core/src/dev-server/health-checker/checkers/typescript-health-checker.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport ts from \"typescript\";\nimport type { FileManager } from \"../../file-manager\";\nimport { tsconfigManager } from \"../../tsconfig-manager\";\nimport type { FileHealthCheckerContract } from \"../file-health-checker.contract\";\nimport { FileHealthResult } from \"../file-health-result\";\nimport { BaseHealthChecker } from \"./base-health-checker\";\n\nexport class TypescriptHealthChecker\n extends BaseHealthChecker\n implements FileHealthCheckerContract\n{\n /**\n * Cached TypeScript program instance\n */\n private program: ts.Program | null = null;\n\n /**\n * Cached parsed TypeScript configuration\n */\n private parsedConfig: ts.ParsedCommandLine | null = null;\n\n /**\n * Health checker name\n */\n public name: string = \"TypeScript\";\n\n /**\n * Path to dedicated worker file for TypeScript checking\n * Runs in a separate thread to avoid blocking the main dev server\n */\n public workerPath: string = \"./workers/ts-health.worker\";\n\n /**\n * Whether checker is initialized\n */\n private initialized: boolean = false;\n\n /**\n * Check if file is a TypeScript file\n */\n private isTypeScriptFile(filePath: string): boolean {\n const ext = filePath.toLowerCase();\n return ext.endsWith(\".ts\") || ext.endsWith(\".tsx\");\n }\n\n /** Preserve files selected by tsconfig when source updates rebuild the program. */\n private getProgramRootNames(files: FileManager[]): string[] {\n return [\n ...new Set([\n ...(this.parsedConfig?.fileNames || []),\n ...files.map((file) => file.absolutePath),\n ]),\n ];\n }\n\n /**\n * Extract line and column from diagnostic location\n */\n private getDiagnosticLocation(diagnostic: ts.Diagnostic): {\n lineNumber: number;\n columnNumber: number;\n } {\n if (diagnostic.file && diagnostic.start !== undefined) {\n const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n return {\n lineNumber: line + 1, // TypeScript uses 0-based, we use 1-based\n columnNumber: character + 1,\n };\n }\n return {\n lineNumber: 1,\n columnNumber: 1,\n };\n }\n\n /**\n * Format diagnostic message\n */\n private formatDiagnosticMessage(diagnostic: ts.Diagnostic): string {\n if (diagnostic.file && diagnostic.start !== undefined) {\n return ts.formatDiagnostic(diagnostic, {\n getCurrentDirectory: () => process.cwd(),\n getCanonicalFileName: (fileName) => fileName,\n getNewLine: () => \"\\n\",\n });\n }\n return diagnostic.messageText.toString();\n }\n\n /**\n * Display health check results in a pretty format\n */\n private displayResults(file: FileManager, result: FileHealthResult): void {\n const stats = result.getStats();\n\n // Only display if there are errors or warnings\n if (stats.errors === 0 && stats.warnings === 0) {\n return;\n }\n\n const fileName = file.relativePath.replace(/\\\\/g, \"/\");\n const errorCount = stats.errors;\n const warningCount = stats.warnings;\n const sourceLines = file.source ? file.source.split(\"\\n\") : [];\n\n // Display header\n // console.log(\n // `\\n${colors.dim(\"╭─\")} ${colors.bold(colors.cyanBright(\"TypeScript Health\"))} ${colors.dim(\"→\")} ${colors.cyan(fileName)}`,\n // );\n\n // Display errors\n if (errorCount > 0) {\n const errorMessages = result.messages.filter((m) => m.type === \"error\");\n for (const error of errorMessages) {\n const icon = colors.redBright(\"✖\");\n const level = colors.redBright(colors.bold(\"ERROR\"));\n console.log(\n `\\n${icon} ${level} ${colors.dim(\"in\")} ${colors.cyanBright(fileName)}${colors.dim(`(${error.lineNumber},${error.columnNumber})`)}`,\n );\n // Extract just the message text (remove file path prefix if present)\n const messageLines = error.message.split(\"\\n\");\n const cleanMessage = messageLines\n .map((line) => line.replace(/^[^:]+:\\d+:\\d+ - /, \"\").trim())\n .filter((line) => line.length > 0)\n .join(\"\\n\");\n console.log(` ${colors.dim(\"→\")} ${colors.red(cleanMessage)}`);\n\n // Display code line with underline indicator\n if (\n sourceLines.length > 0 &&\n error.lineNumber > 0 &&\n error.lineNumber <= sourceLines.length\n ) {\n const lineIndex = error.lineNumber - 1; // Convert to 0-based index\n const lineContent = sourceLines[lineIndex];\n const lineNum = error.lineNumber.toString().padStart(4, \" \");\n const errorLength = error.length || 1;\n const columnIndex = error.columnNumber - 1; // Convert to 0-based index\n\n // Display the line with normal color (not all red)\n console.log(` ${colors.dim(lineNum)} ${colors.dim(\"│\")} ${lineContent || \"\"}`);\n\n // Display underline indicator\n // Padding accounts for: lineNum.length + 1 space + 1 (│) + 1 space = lineNum.length + 3\n // (The initial 2 spaces indent is already in the console.log)\n const prefixPadding = lineNum.length + 3;\n const columnPadding = \" \".repeat(columnIndex);\n const underline = colors.redBright(\"~\".repeat(Math.max(1, errorLength)));\n console.log(` ${colors.dim(\" \".repeat(prefixPadding))}${columnPadding}${underline}`);\n }\n }\n }\n\n // Display warnings\n if (warningCount > 0) {\n const warningMessages = result.messages.filter((m) => m.type === \"warning\");\n for (const warning of warningMessages) {\n const icon = colors.yellowBright(\"⚠\");\n const level = colors.yellowBright(colors.bold(\"WARNING\"));\n console.log(\n `\\n${icon} ${level} ${colors.dim(\"in\")} ${colors.cyanBright(fileName)}${colors.dim(`(${warning.lineNumber},${warning.columnNumber})`)}`,\n );\n // Extract just the message text (remove file path prefix if present)\n const messageLines = warning.message.split(\"\\n\");\n const cleanMessage = messageLines\n .map((line) => line.replace(/^[^:]+:\\d+:\\d+ - /, \"\").trim())\n .filter((line) => line.length > 0)\n .join(\"\\n\");\n console.log(` ${colors.dim(\"→\")} ${colors.yellow(cleanMessage)}`);\n\n // Display code line with underline indicator\n if (\n sourceLines.length > 0 &&\n warning.lineNumber > 0 &&\n warning.lineNumber <= sourceLines.length\n ) {\n const lineIndex = warning.lineNumber - 1; // Convert to 0-based index\n const lineContent = sourceLines[lineIndex];\n const lineNum = warning.lineNumber.toString().padStart(4, \" \");\n const warningLength = warning.length || 1;\n const columnIndex = warning.columnNumber - 1; // Convert to 0-based index\n\n // Display the line with normal color (not all yellow)\n console.log(` ${colors.dim(lineNum)} ${colors.dim(\"│\")} ${lineContent || \"\"}`);\n\n // Display underline indicator\n // Padding accounts for: lineNum.length + 1 space + 1 (│) + 1 space = lineNum.length + 3\n // (The initial 2 spaces indent is already in the console.log)\n const prefixPadding = lineNum.length + 3;\n const columnPadding = \" \".repeat(columnIndex);\n const underline = colors.yellowBright(\"~\".repeat(Math.max(1, warningLength)));\n console.log(` ${colors.dim(\" \".repeat(prefixPadding))}${columnPadding}${underline}`);\n }\n }\n }\n\n // Display summary\n const summary = [];\n if (errorCount > 0) {\n summary.push(colors.red(`${errorCount} error${errorCount > 1 ? \"s\" : \"\"}`));\n }\n if (warningCount > 0) {\n summary.push(colors.yellow(`${warningCount} warning${warningCount > 1 ? \"s\" : \"\"}`));\n }\n\n // console.log(\n // `\\n${colors.dim(\"╰─\")} ${colors.bold(\"TypeScript\")} ${colors.dim(\"→\")} ${summary.join(colors.dim(\" and \"))}`,\n // );\n }\n\n /**\n * Detect when files are changed\n */\n public async onFileChanges(files: FileManager[]): Promise<void> {\n // recreate an incremental program\n if (!this.parsedConfig) {\n return;\n }\n\n this.program = ts.createProgram(\n this.getProgramRootNames(files),\n {\n ...this.parsedConfig.options,\n incremental: true,\n },\n undefined,\n this.program!,\n );\n }\n\n /**\n * Initialize the health checker\n */\n public initialize(): TypescriptHealthChecker {\n try {\n // Verify tsconfigManager is initialized and has config\n if (!tsconfigManager.tsconfig || Object.keys(tsconfigManager.tsconfig).length === 0) {\n this.initialized = true;\n return this;\n }\n\n // Parse the config using tsconfigManager's cached config\n this.parsedConfig = ts.parseJsonConfigFileContent(\n tsconfigManager.tsconfig,\n ts.sys,\n process.cwd(),\n );\n\n // Check for config errors\n if (this.parsedConfig.errors.length > 0) {\n // Log config errors but continue (will skip validation)\n console.warn(\n \"TypeScript Health Checker: tsconfig.json has errors:\",\n this.parsedConfig.errors.map((e) => ts.formatDiagnostic(e, ts.createCompilerHost({}))),\n );\n }\n\n this.program = ts.createProgram(this.parsedConfig.fileNames, this.parsedConfig.options);\n\n this.initialized = true;\n } catch (error) {\n // Handle any errors during initialization gracefully\n console.warn(\"TypeScript Health Checker: Failed to initialize:\", error);\n this.initialized = true; // Mark as initialized to prevent retries\n }\n\n return this;\n }\n\n /**\n * Validate the health of the file\n */\n public async validate(file: FileManager, result: FileHealthResult): Promise<FileHealthResult> {\n // Early exit: skip non-TypeScript files\n if (!this.isTypeScriptFile(file.absolutePath)) {\n result.markAsHealthy();\n return result;\n }\n\n // Early exit: check if parsed config is available\n if (!this.parsedConfig) {\n result.markAsHealthy();\n return result;\n }\n\n try {\n // Lazy-load program on first validation if not already created\n if (!this.program) {\n // Create program with all project files from parsed config (for proper type checking)\n this.program = ts.createProgram(this.parsedConfig.fileNames, this.parsedConfig.options);\n }\n\n // Get source file from program\n const sourceFile = this.program.getSourceFile(file.absolutePath);\n\n // If file is not in the program, return healthy (might be excluded)\n if (!sourceFile) {\n result.markAsHealthy();\n return result;\n }\n\n // Get diagnostics for the specific file only\n const syntacticDiagnostics = this.program.getSyntacticDiagnostics(sourceFile);\n const semanticDiagnostics = this.program.getSemanticDiagnostics(sourceFile);\n\n // Combine all diagnostics\n const allDiagnostics = [...syntacticDiagnostics, ...semanticDiagnostics];\n\n // Convert TypeScript diagnostics to FileHealthResult format\n const errors: Array<{\n message: string;\n type: \"error\";\n lineNumber: number;\n columnNumber: number;\n length: number;\n }> = [];\n const warnings: Array<{\n message: string;\n type: \"warning\";\n lineNumber: number;\n columnNumber: number;\n length: number;\n }> = [];\n\n for (const diagnostic of allDiagnostics) {\n const location = this.getDiagnosticLocation(diagnostic);\n const message = this.formatDiagnosticMessage(diagnostic);\n const errorLength = diagnostic.length || 1;\n\n if (diagnostic.category === ts.DiagnosticCategory.Error) {\n errors.push({\n message,\n type: \"error\",\n lineNumber: location.lineNumber,\n columnNumber: location.columnNumber,\n length: errorLength,\n });\n } else if (diagnostic.category === ts.DiagnosticCategory.Warning) {\n warnings.push({\n message,\n type: \"warning\",\n lineNumber: location.lineNumber,\n columnNumber: location.columnNumber,\n length: errorLength,\n });\n }\n }\n\n // Add errors and warnings to result\n if (errors.length > 0) {\n result.addErrors(errors);\n }\n if (warnings.length > 0) {\n result.addWarnings(warnings);\n }\n\n // If no errors or warnings, mark as healthy\n if (errors.length === 0 && warnings.length === 0) {\n result.markAsHealthy();\n } else {\n // Display results if there are errors or warnings\n this.displayResults(file, result);\n }\n } catch (error) {\n // Handle any errors during validation gracefully\n console.warn(`TypeScript Health Checker: Error validating file ${file.relativePath}:`, error);\n result.markAsHealthy(); // Return healthy on error to avoid blocking\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import ts from "typescript";
|
|
2
3
|
import { parentPort, workerData } from "worker_threads";
|
|
3
4
|
|
|
@@ -22,6 +23,7 @@ var TypeScriptHealthWorker = class {
|
|
|
22
23
|
this.program = null;
|
|
23
24
|
this.parsedConfig = null;
|
|
24
25
|
this.fileContents = /* @__PURE__ */ new Map();
|
|
26
|
+
this.deletedRootPaths = /* @__PURE__ */ new Set();
|
|
25
27
|
this.initialized = false;
|
|
26
28
|
this.cwd = process.cwd();
|
|
27
29
|
}
|
|
@@ -63,8 +65,11 @@ var TypeScriptHealthWorker = class {
|
|
|
63
65
|
errors: [],
|
|
64
66
|
warnings: []
|
|
65
67
|
}));
|
|
66
|
-
for (const file of files)
|
|
67
|
-
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
this.fileContents.set(file.path, file.content);
|
|
70
|
+
this.deletedRootPaths.delete(this.getPathIdentity(file.path));
|
|
71
|
+
}
|
|
72
|
+
this.program = ts.createProgram(this.getProgramRootNames(), this.parsedConfig.options, this.createCompilerHost(), this.program || void 0);
|
|
68
73
|
const results = [];
|
|
69
74
|
for (const file of files) {
|
|
70
75
|
const result = this.checkSingleFile(file);
|
|
@@ -76,19 +81,28 @@ var TypeScriptHealthWorker = class {
|
|
|
76
81
|
* Handle file changes (update program incrementally)
|
|
77
82
|
*/
|
|
78
83
|
handleFileChanges(files) {
|
|
79
|
-
for (const file of files)
|
|
80
|
-
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
this.fileContents.set(file.path, file.content);
|
|
86
|
+
this.deletedRootPaths.delete(this.getPathIdentity(file.path));
|
|
87
|
+
}
|
|
88
|
+
if (this.parsedConfig && this.program) this.program = ts.createProgram(this.getProgramRootNames(), this.parsedConfig.options, this.createCompilerHost(), this.program || void 0);
|
|
81
89
|
}
|
|
82
90
|
/**
|
|
83
91
|
* Handle deleted files (remove from cache)
|
|
84
92
|
*/
|
|
85
93
|
handleFilesDeleted(files) {
|
|
86
94
|
let hasChanges = false;
|
|
87
|
-
for (const file of files)
|
|
88
|
-
this.
|
|
89
|
-
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const identity = this.getPathIdentity(file.path);
|
|
97
|
+
const cachedPath = Array.from(this.fileContents.keys()).find((candidate) => this.getPathIdentity(candidate) === identity);
|
|
98
|
+
if (cachedPath) {
|
|
99
|
+
this.fileContents.delete(cachedPath);
|
|
100
|
+
hasChanges = true;
|
|
101
|
+
}
|
|
102
|
+
if (!this.deletedRootPaths.has(identity)) hasChanges = true;
|
|
103
|
+
this.deletedRootPaths.add(identity);
|
|
90
104
|
}
|
|
91
|
-
if (hasChanges && this.parsedConfig) this.program = ts.createProgram(
|
|
105
|
+
if (hasChanges && this.parsedConfig) this.program = ts.createProgram(this.getProgramRootNames(), this.parsedConfig.options, this.createCompilerHost(), this.program || void 0);
|
|
92
106
|
}
|
|
93
107
|
/**
|
|
94
108
|
* Check a single file for diagnostics
|
|
@@ -168,6 +182,21 @@ var TypeScriptHealthWorker = class {
|
|
|
168
182
|
}
|
|
169
183
|
};
|
|
170
184
|
}
|
|
185
|
+
/** Keep tsconfig-selected declarations in every incremental program. */
|
|
186
|
+
getProgramRootNames() {
|
|
187
|
+
const configuredRoots = this.parsedConfig?.fileNames || [];
|
|
188
|
+
const rootsByIdentity = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const filePath of [...configuredRoots, ...this.fileContents.keys()]) {
|
|
190
|
+
const identity = this.getPathIdentity(filePath);
|
|
191
|
+
if (!this.deletedRootPaths.has(identity)) rootsByIdentity.set(identity, filePath);
|
|
192
|
+
}
|
|
193
|
+
return [...rootsByIdentity.values()];
|
|
194
|
+
}
|
|
195
|
+
/** Compare equivalent Windows path spellings as one TypeScript root. */
|
|
196
|
+
getPathIdentity(filePath) {
|
|
197
|
+
const normalized = path.normalize(path.resolve(filePath));
|
|
198
|
+
return ts.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();
|
|
199
|
+
}
|
|
171
200
|
};
|
|
172
201
|
const worker = new TypeScriptHealthWorker();
|
|
173
202
|
parentPort?.on("message", (message) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts-health.worker.mjs","names":[],"sources":["../../../../../../../../../core/src/dev-server/health-checker/workers/ts-health.worker.ts"],"sourcesContent":["/**\n * TypeScript Health Check Worker\n *\n * This worker runs in a dedicated thread to perform TypeScript type checking\n * without blocking the main dev server thread. It maintains a persistent\n * ts.Program instance for fast incremental type checking.\n *\n * Communication:\n * - Receives: { type: 'init' | 'check' | 'fileChanges' | 'shutdown', ... }\n * - Sends: { type: 'results' | 'initialized' | 'error', ... }\n */\nimport ts from \"typescript\";\nimport { parentPort, workerData } from \"worker_threads\";\n\n/**\n * Serialized file data received from main thread\n */\ntype SerializedFile = {\n /** Absolute file path */\n path: string;\n /** File content (source code) */\n content: string;\n /** Relative path for display */\n relativePath: string;\n};\n\n/**\n * Diagnostic message sent back to main thread\n */\ntype DiagnosticMessage = {\n type: \"error\" | \"warning\";\n message: string;\n lineNumber: number;\n columnNumber: number;\n length: number;\n filePath: string;\n relativePath: string;\n};\n\n/**\n * Health check result for a single file\n */\ntype FileCheckResult = {\n path: string;\n relativePath: string;\n healthy: boolean;\n errors: DiagnosticMessage[];\n warnings: DiagnosticMessage[];\n};\n\n/**\n * Messages received from main thread\n */\n/**\n * Deleted file info\n */\ntype DeletedFile = {\n path: string;\n relativePath: string;\n};\n\n/**\n * Messages received from main thread\n */\ntype WorkerMessage =\n | { type: \"init\"; config: { cwd: string; tsconfigPath?: string } }\n | { type: \"check\"; files: SerializedFile[] }\n | { type: \"fileChanges\"; files: SerializedFile[] }\n | { type: \"filesDeleted\"; files: DeletedFile[] }\n | { type: \"shutdown\" };\n\n/**\n * Messages sent to main thread\n */\ntype WorkerResponse =\n | { type: \"initialized\"; success: boolean; error?: string }\n | { type: \"results\"; results: FileCheckResult[] }\n | { type: \"error\"; message: string };\n\n/**\n * TypeScript Health Worker class\n * Maintains persistent ts.Program for incremental type checking\n */\nclass TypeScriptHealthWorker {\n /**\n * Cached TypeScript program instance\n */\n private program: ts.Program | null = null;\n\n /**\n * Parsed TypeScript configuration\n */\n private parsedConfig: ts.ParsedCommandLine | null = null;\n\n /**\n * In-memory file contents cache\n */\n private fileContents = new Map<string, string>();\n\n /**\n * Whether the worker is initialized\n */\n private initialized = false;\n\n /**\n * Current working directory\n */\n private cwd: string = process.cwd();\n\n /**\n * Initialize the worker with TypeScript configuration\n */\n public initialize(config: { cwd: string; tsconfigPath?: string }): boolean {\n try {\n this.cwd = config.cwd || process.cwd();\n\n // Try to load tsconfig.json\n const tsconfigPath = config.tsconfigPath || ts.findConfigFile(this.cwd, ts.sys.fileExists);\n\n if (!tsconfigPath) {\n this.initialized = true;\n return true; // No tsconfig, will skip type checking\n }\n\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\n\n if (configFile.error) {\n console.warn(\"TypeScript Worker: Error reading tsconfig:\", configFile.error);\n this.initialized = true;\n return true;\n }\n\n this.parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, this.cwd);\n\n if (this.parsedConfig.errors.length > 0) {\n console.warn(\"TypeScript Worker: tsconfig has errors:\", this.parsedConfig.errors);\n }\n\n this.initialized = true;\n return true;\n } catch (error) {\n console.error(\"TypeScript Worker: Failed to initialize:\", error);\n this.initialized = true;\n return false;\n }\n }\n\n /**\n * Check files for TypeScript errors\n */\n public checkFiles(files: SerializedFile[]): FileCheckResult[] {\n if (!this.parsedConfig) {\n // No config, return all files as healthy\n return files.map((file) => ({\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n }));\n }\n\n // Update in-memory file cache\n for (const file of files) {\n this.fileContents.set(file.path, file.content);\n }\n\n // Create or update the program (incremental)\n this.program = ts.createProgram(\n Array.from(this.fileContents.keys()),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined, // Pass old program for incremental compilation\n );\n\n // Check each file\n const results: FileCheckResult[] = [];\n\n for (const file of files) {\n const result = this.checkSingleFile(file);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * Handle file changes (update program incrementally)\n */\n public handleFileChanges(files: SerializedFile[]): void {\n for (const file of files) {\n this.fileContents.set(file.path, file.content);\n }\n\n // Recreate program with new file contents\n if (this.parsedConfig && this.program) {\n this.program = ts.createProgram(\n Array.from(this.fileContents.keys()),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined,\n );\n }\n }\n\n /**\n * Handle deleted files (remove from cache)\n */\n public handleFilesDeleted(files: DeletedFile[]): void {\n let hasChanges = false;\n\n for (const file of files) {\n if (this.fileContents.has(file.path)) {\n this.fileContents.delete(file.path);\n hasChanges = true;\n }\n }\n\n // Recreate program without deleted files\n if (hasChanges && this.parsedConfig) {\n this.program = ts.createProgram(\n Array.from(this.fileContents.keys()),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined,\n );\n }\n }\n\n /**\n * Check a single file for diagnostics\n */\n private checkSingleFile(file: SerializedFile): FileCheckResult {\n if (!this.program) {\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n };\n }\n\n const sourceFile = this.program.getSourceFile(file.path);\n\n if (!sourceFile) {\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n };\n }\n\n const syntacticDiagnostics = this.program.getSyntacticDiagnostics(sourceFile);\n const semanticDiagnostics = this.program.getSemanticDiagnostics(sourceFile);\n const allDiagnostics = [...syntacticDiagnostics, ...semanticDiagnostics];\n\n const errors: DiagnosticMessage[] = [];\n const warnings: DiagnosticMessage[] = [];\n\n for (const diagnostic of allDiagnostics) {\n const message = this.formatDiagnostic(diagnostic, file);\n\n if (diagnostic.category === ts.DiagnosticCategory.Error) {\n errors.push(message);\n } else if (diagnostic.category === ts.DiagnosticCategory.Warning) {\n warnings.push(message);\n }\n }\n\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: errors.length === 0 && warnings.length === 0,\n errors,\n warnings,\n };\n }\n\n /**\n * Format a TypeScript diagnostic into our message format\n */\n private formatDiagnostic(diagnostic: ts.Diagnostic, file: SerializedFile): DiagnosticMessage {\n let lineNumber = 1;\n let columnNumber = 1;\n let length = 1;\n\n if (diagnostic.file && diagnostic.start !== undefined) {\n const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n lineNumber = pos.line + 1;\n columnNumber = pos.character + 1;\n length = diagnostic.length || 1;\n }\n\n const messageText =\n typeof diagnostic.messageText === \"string\"\n ? diagnostic.messageText\n : diagnostic.messageText.messageText;\n\n return {\n type: diagnostic.category === ts.DiagnosticCategory.Error ? \"error\" : \"warning\",\n message: messageText,\n lineNumber,\n columnNumber,\n length,\n filePath: file.path,\n relativePath: file.relativePath,\n };\n }\n\n /**\n * Create a custom compiler host that reads from in-memory cache\n */\n private createCompilerHost(): ts.CompilerHost {\n const defaultHost = ts.createCompilerHost(this.parsedConfig?.options || {});\n\n return {\n ...defaultHost,\n readFile: (fileName: string) => {\n // Check in-memory cache first\n if (this.fileContents.has(fileName)) {\n return this.fileContents.get(fileName);\n }\n // Fall back to disk\n return defaultHost.readFile(fileName);\n },\n fileExists: (fileName: string) => {\n if (this.fileContents.has(fileName)) {\n return true;\n }\n return defaultHost.fileExists(fileName);\n },\n };\n }\n}\n\n// Create worker instance\nconst worker = new TypeScriptHealthWorker();\n\n// Handle messages from main thread\nparentPort?.on(\"message\", (message: WorkerMessage) => {\n try {\n switch (message.type) {\n case \"init\": {\n const success = worker.initialize(message.config);\n const response: WorkerResponse = { type: \"initialized\", success };\n parentPort?.postMessage(response);\n break;\n }\n\n case \"check\": {\n const results = worker.checkFiles(message.files);\n const response: WorkerResponse = { type: \"results\", results };\n parentPort?.postMessage(response);\n break;\n }\n\n case \"fileChanges\": {\n worker.handleFileChanges(message.files);\n break;\n }\n\n case \"filesDeleted\": {\n worker.handleFilesDeleted(message.files);\n break;\n }\n\n case \"shutdown\": {\n process.exit(0);\n }\n }\n } catch (error) {\n const response: WorkerResponse = {\n type: \"error\",\n message: error instanceof Error ? error.message : String(error),\n };\n parentPort?.postMessage(response);\n }\n});\n\n// Handle initialization from workerData if provided\nif (workerData?.autoInit) {\n worker.initialize({ cwd: workerData.cwd || process.cwd() });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ts-health.worker.mjs","names":[],"sources":["../../../../../../../../../core/src/dev-server/health-checker/workers/ts-health.worker.ts"],"sourcesContent":["/**\n * TypeScript Health Check Worker\n *\n * This worker runs in a dedicated thread to perform TypeScript type checking\n * without blocking the main dev server thread. It maintains a persistent\n * ts.Program instance for fast incremental type checking.\n *\n * Communication:\n * - Receives: { type: 'init' | 'check' | 'fileChanges' | 'shutdown', ... }\n * - Sends: { type: 'results' | 'initialized' | 'error', ... }\n */\nimport ts from \"typescript\";\nimport path from \"node:path\";\nimport { parentPort, workerData } from \"worker_threads\";\n\n/**\n * Serialized file data received from main thread\n */\ntype SerializedFile = {\n /** Absolute file path */\n path: string;\n /** File content (source code) */\n content: string;\n /** Relative path for display */\n relativePath: string;\n};\n\n/**\n * Diagnostic message sent back to main thread\n */\ntype DiagnosticMessage = {\n type: \"error\" | \"warning\";\n message: string;\n lineNumber: number;\n columnNumber: number;\n length: number;\n filePath: string;\n relativePath: string;\n};\n\n/**\n * Health check result for a single file\n */\ntype FileCheckResult = {\n path: string;\n relativePath: string;\n healthy: boolean;\n errors: DiagnosticMessage[];\n warnings: DiagnosticMessage[];\n};\n\n/**\n * Messages received from main thread\n */\n/**\n * Deleted file info\n */\ntype DeletedFile = {\n path: string;\n relativePath: string;\n};\n\n/**\n * Messages received from main thread\n */\ntype WorkerMessage =\n | { type: \"init\"; config: { cwd: string; tsconfigPath?: string } }\n | { type: \"check\"; files: SerializedFile[] }\n | { type: \"fileChanges\"; files: SerializedFile[] }\n | { type: \"filesDeleted\"; files: DeletedFile[] }\n | { type: \"shutdown\" };\n\n/**\n * Messages sent to main thread\n */\ntype WorkerResponse =\n | { type: \"initialized\"; success: boolean; error?: string }\n | { type: \"results\"; results: FileCheckResult[] }\n | { type: \"error\"; message: string };\n\n/**\n * TypeScript Health Worker class\n * Maintains persistent ts.Program for incremental type checking\n */\nclass TypeScriptHealthWorker {\n /**\n * Cached TypeScript program instance\n */\n private program: ts.Program | null = null;\n\n /**\n * Parsed TypeScript configuration\n */\n private parsedConfig: ts.ParsedCommandLine | null = null;\n\n /**\n * In-memory file contents cache\n */\n private fileContents = new Map<string, string>();\n\n /** Source paths removed since the tsconfig was parsed. */\n private deletedRootPaths = new Set<string>();\n\n /**\n * Whether the worker is initialized\n */\n private initialized = false;\n\n /**\n * Current working directory\n */\n private cwd: string = process.cwd();\n\n /**\n * Initialize the worker with TypeScript configuration\n */\n public initialize(config: { cwd: string; tsconfigPath?: string }): boolean {\n try {\n this.cwd = config.cwd || process.cwd();\n\n // Try to load tsconfig.json\n const tsconfigPath = config.tsconfigPath || ts.findConfigFile(this.cwd, ts.sys.fileExists);\n\n if (!tsconfigPath) {\n this.initialized = true;\n return true; // No tsconfig, will skip type checking\n }\n\n const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);\n\n if (configFile.error) {\n console.warn(\"TypeScript Worker: Error reading tsconfig:\", configFile.error);\n this.initialized = true;\n return true;\n }\n\n this.parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, this.cwd);\n\n if (this.parsedConfig.errors.length > 0) {\n console.warn(\"TypeScript Worker: tsconfig has errors:\", this.parsedConfig.errors);\n }\n\n this.initialized = true;\n return true;\n } catch (error) {\n console.error(\"TypeScript Worker: Failed to initialize:\", error);\n this.initialized = true;\n return false;\n }\n }\n\n /**\n * Check files for TypeScript errors\n */\n public checkFiles(files: SerializedFile[]): FileCheckResult[] {\n if (!this.parsedConfig) {\n // No config, return all files as healthy\n return files.map((file) => ({\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n }));\n }\n\n // Update in-memory file cache\n for (const file of files) {\n this.fileContents.set(file.path, file.content);\n this.deletedRootPaths.delete(this.getPathIdentity(file.path));\n }\n\n // Create or update the program (incremental)\n this.program = ts.createProgram(\n this.getProgramRootNames(),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined, // Pass old program for incremental compilation\n );\n\n // Check each file\n const results: FileCheckResult[] = [];\n\n for (const file of files) {\n const result = this.checkSingleFile(file);\n results.push(result);\n }\n\n return results;\n }\n\n /**\n * Handle file changes (update program incrementally)\n */\n public handleFileChanges(files: SerializedFile[]): void {\n for (const file of files) {\n this.fileContents.set(file.path, file.content);\n this.deletedRootPaths.delete(this.getPathIdentity(file.path));\n }\n\n // Recreate program with new file contents\n if (this.parsedConfig && this.program) {\n this.program = ts.createProgram(\n this.getProgramRootNames(),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined,\n );\n }\n }\n\n /**\n * Handle deleted files (remove from cache)\n */\n public handleFilesDeleted(files: DeletedFile[]): void {\n let hasChanges = false;\n\n for (const file of files) {\n const identity = this.getPathIdentity(file.path);\n const cachedPath = Array.from(this.fileContents.keys()).find(\n (candidate) => this.getPathIdentity(candidate) === identity,\n );\n\n if (cachedPath) {\n this.fileContents.delete(cachedPath);\n hasChanges = true;\n }\n if (!this.deletedRootPaths.has(identity)) hasChanges = true;\n this.deletedRootPaths.add(identity);\n }\n\n // Recreate program without deleted files\n if (hasChanges && this.parsedConfig) {\n this.program = ts.createProgram(\n this.getProgramRootNames(),\n this.parsedConfig.options,\n this.createCompilerHost(),\n this.program || undefined,\n );\n }\n }\n\n /**\n * Check a single file for diagnostics\n */\n private checkSingleFile(file: SerializedFile): FileCheckResult {\n if (!this.program) {\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n };\n }\n\n const sourceFile = this.program.getSourceFile(file.path);\n\n if (!sourceFile) {\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: true,\n errors: [],\n warnings: [],\n };\n }\n\n const syntacticDiagnostics = this.program.getSyntacticDiagnostics(sourceFile);\n const semanticDiagnostics = this.program.getSemanticDiagnostics(sourceFile);\n const allDiagnostics = [...syntacticDiagnostics, ...semanticDiagnostics];\n\n const errors: DiagnosticMessage[] = [];\n const warnings: DiagnosticMessage[] = [];\n\n for (const diagnostic of allDiagnostics) {\n const message = this.formatDiagnostic(diagnostic, file);\n\n if (diagnostic.category === ts.DiagnosticCategory.Error) {\n errors.push(message);\n } else if (diagnostic.category === ts.DiagnosticCategory.Warning) {\n warnings.push(message);\n }\n }\n\n return {\n path: file.path,\n relativePath: file.relativePath,\n healthy: errors.length === 0 && warnings.length === 0,\n errors,\n warnings,\n };\n }\n\n /**\n * Format a TypeScript diagnostic into our message format\n */\n private formatDiagnostic(diagnostic: ts.Diagnostic, file: SerializedFile): DiagnosticMessage {\n let lineNumber = 1;\n let columnNumber = 1;\n let length = 1;\n\n if (diagnostic.file && diagnostic.start !== undefined) {\n const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);\n lineNumber = pos.line + 1;\n columnNumber = pos.character + 1;\n length = diagnostic.length || 1;\n }\n\n const messageText =\n typeof diagnostic.messageText === \"string\"\n ? diagnostic.messageText\n : diagnostic.messageText.messageText;\n\n return {\n type: diagnostic.category === ts.DiagnosticCategory.Error ? \"error\" : \"warning\",\n message: messageText,\n lineNumber,\n columnNumber,\n length,\n filePath: file.path,\n relativePath: file.relativePath,\n };\n }\n\n /**\n * Create a custom compiler host that reads from in-memory cache\n */\n private createCompilerHost(): ts.CompilerHost {\n const defaultHost = ts.createCompilerHost(this.parsedConfig?.options || {});\n\n return {\n ...defaultHost,\n readFile: (fileName: string) => {\n // Check in-memory cache first\n if (this.fileContents.has(fileName)) {\n return this.fileContents.get(fileName);\n }\n // Fall back to disk\n return defaultHost.readFile(fileName);\n },\n fileExists: (fileName: string) => {\n if (this.fileContents.has(fileName)) {\n return true;\n }\n return defaultHost.fileExists(fileName);\n },\n };\n }\n\n /** Keep tsconfig-selected declarations in every incremental program. */\n private getProgramRootNames(): string[] {\n const configuredRoots = this.parsedConfig?.fileNames || [];\n\n const rootsByIdentity = new Map<string, string>();\n\n for (const filePath of [...configuredRoots, ...this.fileContents.keys()]) {\n const identity = this.getPathIdentity(filePath);\n if (!this.deletedRootPaths.has(identity)) rootsByIdentity.set(identity, filePath);\n }\n\n return [...rootsByIdentity.values()];\n }\n\n /** Compare equivalent Windows path spellings as one TypeScript root. */\n private getPathIdentity(filePath: string): string {\n const normalized = path.normalize(path.resolve(filePath));\n\n return ts.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();\n }\n}\n\n// Create worker instance\nconst worker = new TypeScriptHealthWorker();\n\n// Handle messages from main thread\nparentPort?.on(\"message\", (message: WorkerMessage) => {\n try {\n switch (message.type) {\n case \"init\": {\n const success = worker.initialize(message.config);\n const response: WorkerResponse = { type: \"initialized\", success };\n parentPort?.postMessage(response);\n break;\n }\n\n case \"check\": {\n const results = worker.checkFiles(message.files);\n const response: WorkerResponse = { type: \"results\", results };\n parentPort?.postMessage(response);\n break;\n }\n\n case \"fileChanges\": {\n worker.handleFileChanges(message.files);\n break;\n }\n\n case \"filesDeleted\": {\n worker.handleFilesDeleted(message.files);\n break;\n }\n\n case \"shutdown\": {\n process.exit(0);\n }\n }\n } catch (error) {\n const response: WorkerResponse = {\n type: \"error\",\n message: error instanceof Error ? error.message : String(error),\n };\n parentPort?.postMessage(response);\n }\n});\n\n// Handle initialization from workerData if provided\nif (workerData?.autoInit) {\n worker.initialize({ cwd: workerData.cwd || process.cwd() });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region ../core/src/dev-server/translation-keys-sources.ts
|
|
2
|
+
const GROUPED_TRANSLATIONS_MARKER = "groupedTranslations";
|
|
3
|
+
/**
|
|
4
|
+
* Cheap pre-filter for candidate translation-registering source files.
|
|
5
|
+
*
|
|
6
|
+
* `groupedTranslations` calls used to be scanned for on dedicated
|
|
7
|
+
* `src/**/utils/locales.*` files only, so a web module registering keys
|
|
8
|
+
* elsewhere (e.g. `src/web/layout.register.ts`) was silently invisible to the
|
|
9
|
+
* generator. Any app source file can call it, so discovery has to consider
|
|
10
|
+
* every file — this text check keeps that cheap by skipping the AST parse
|
|
11
|
+
* for the files that plainly do not call it, without executing app code.
|
|
12
|
+
*/
|
|
13
|
+
function isTranslationRegisteringSource(source) {
|
|
14
|
+
return source.includes(GROUPED_TRANSLATIONS_MARKER);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { isTranslationRegisteringSource };
|
|
19
|
+
//# sourceMappingURL=translation-keys-sources.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation-keys-sources.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/translation-keys-sources.ts"],"sourcesContent":["const GROUPED_TRANSLATIONS_MARKER = \"groupedTranslations\";\n\n/**\n * Cheap pre-filter for candidate translation-registering source files.\n *\n * `groupedTranslations` calls used to be scanned for on dedicated\n * `src/**/utils/locales.*` files only, so a web module registering keys\n * elsewhere (e.g. `src/web/layout.register.ts`) was silently invisible to the\n * generator. Any app source file can call it, so discovery has to consider\n * every file — this text check keeps that cheap by skipping the AST parse\n * for the files that plainly do not call it, without executing app code.\n */\nexport function isTranslationRegisteringSource(source: string): boolean {\n return source.includes(GROUPED_TRANSLATIONS_MARKER);\n}\n"],"mappings":";AAAA,MAAM,8BAA8B;;;;;;;;;;;AAYpC,SAAgB,+BAA+B,QAAyB;CACtE,OAAO,OAAO,SAAS,2BAA2B;AACpD"}
|
|
@@ -5,14 +5,41 @@ import { devLogError, devLogInfo, devLogSuccess, devServeLog } from "./dev-logge
|
|
|
5
5
|
import { filesOrchestrator } from "./files-orchestrator.mjs";
|
|
6
6
|
import { readConfigAst } from "./read-config-ast.mjs";
|
|
7
7
|
import { runTypingsGeneration } from "./run-typings-generation.mjs";
|
|
8
|
-
import { extractTranslationKeys } from "./translation-
|
|
8
|
+
import { extractTranslationKeys } from "./extract-translation-keys.mjs";
|
|
9
|
+
import { isTranslationRegisteringSource } from "./translation-keys-sources.mjs";
|
|
9
10
|
import { join, resolve } from "path";
|
|
10
11
|
import { ensureDirectoryAsync } from "@warlock.js/fs";
|
|
11
12
|
import { constants } from "fs";
|
|
12
13
|
import { access, readFile, writeFile } from "fs/promises";
|
|
13
14
|
import ts from "typescript";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { moduleResolve } from "import-meta-resolve";
|
|
14
17
|
|
|
15
18
|
//#region ../core/src/dev-server/type-generator.ts
|
|
19
|
+
const WEB_PACKAGE = "@warlock.js/web";
|
|
20
|
+
const WEB_BUILD_PACKAGE = "@warlock.js/web/build";
|
|
21
|
+
function isAbsentOptionalWebPackage(thrown) {
|
|
22
|
+
return typeof thrown === "object" && thrown !== null && "code" in thrown && thrown.code === "ERR_MODULE_NOT_FOUND" && thrown instanceof Error && thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`);
|
|
23
|
+
}
|
|
24
|
+
/** Load the build-only web key lister without creating a core-to-web static edge. */
|
|
25
|
+
async function listRouteLocaleKeysForTypeGeneration(appRoot = process.cwd()) {
|
|
26
|
+
const projectUrl = pathToFileURL(resolve(appRoot, "package.json"));
|
|
27
|
+
try {
|
|
28
|
+
moduleResolve(WEB_PACKAGE, projectUrl);
|
|
29
|
+
} catch (thrown) {
|
|
30
|
+
if (isAbsentOptionalWebPackage(thrown)) return [];
|
|
31
|
+
throw thrown;
|
|
32
|
+
}
|
|
33
|
+
const buildModule = await import(moduleResolve(WEB_BUILD_PACKAGE, projectUrl).href);
|
|
34
|
+
if (typeof buildModule.listRouteLocaleKeys !== "function") throw new Error(`${WEB_BUILD_PACKAGE} does not export listRouteLocaleKeys. Upgrade @warlock.js/web to a compatible version.`);
|
|
35
|
+
return buildModule.listRouteLocaleKeys({ appRoot });
|
|
36
|
+
}
|
|
37
|
+
/** True only for the route-locales convention under the canonical web root. */
|
|
38
|
+
function isRouteLocalesJsonPath(path) {
|
|
39
|
+
const normalized = Path.normalize(path);
|
|
40
|
+
const relativePath = normalized.startsWith("src/") ? normalized : Path.toRelative(normalized);
|
|
41
|
+
return /^src\/web(?:\/.*)?\/locales\.json$/.test(relativePath);
|
|
42
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* TypeGenerator - Generates TypeScript type definitions from config files
|
|
18
45
|
*
|
|
@@ -27,6 +54,7 @@ var TypeGenerator = class {
|
|
|
27
54
|
this.manifestPath = join(this.outputDir, "typings-manifest.json");
|
|
28
55
|
this.manifest = null;
|
|
29
56
|
this.configCache = /* @__PURE__ */ new Map();
|
|
57
|
+
this.translationSourceFiles = /* @__PURE__ */ new Set();
|
|
30
58
|
}
|
|
31
59
|
/**
|
|
32
60
|
* Generate all framework type definitions
|
|
@@ -134,7 +162,7 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
|
|
|
134
162
|
* Check if a file change should trigger type regeneration
|
|
135
163
|
*/
|
|
136
164
|
shouldRegenerateTypes(changedPath) {
|
|
137
|
-
return changedPath.includes("src/config/") || changedPath.includes("config/") || this.isLocalesFile(changedPath);
|
|
165
|
+
return changedPath.includes("src/config/") || changedPath.includes("config/") || this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath);
|
|
138
166
|
}
|
|
139
167
|
/**
|
|
140
168
|
* Handle file change - uses incremental update via cache
|
|
@@ -146,7 +174,7 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
|
|
|
146
174
|
await this.saveManifest();
|
|
147
175
|
return;
|
|
148
176
|
}
|
|
149
|
-
if (this.isLocalesFile(changedPath)) {
|
|
177
|
+
if (this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath)) {
|
|
150
178
|
await this.generateTranslationTypes();
|
|
151
179
|
return;
|
|
152
180
|
}
|
|
@@ -191,16 +219,30 @@ ${driverKeys.map((k) => ` ${this.toInterfaceKey(k)}: true;`).join("\n")}
|
|
|
191
219
|
devServeLog(`âš ï¸ Failed to generate config types: ${error}`);
|
|
192
220
|
}
|
|
193
221
|
}
|
|
194
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Generate web's app-augmented translation-key registry.
|
|
224
|
+
*
|
|
225
|
+
* Scans every discovered app source file — not only dedicated
|
|
226
|
+
* `utils/locales.*` files — for `groupedTranslations` calls, both the
|
|
227
|
+
* named-group and object forms, at any nesting depth. Dynamic group names,
|
|
228
|
+
* dictionaries, or keys cannot be resolved statically and are skipped; an
|
|
229
|
+
* app relying on those must document them itself.
|
|
230
|
+
*/
|
|
195
231
|
async generateTranslationTypes() {
|
|
196
232
|
const keys = /* @__PURE__ */ new Set();
|
|
233
|
+
const sourceFiles = /* @__PURE__ */ new Set();
|
|
197
234
|
for (const [path, fileManager] of filesOrchestrator.getFiles()) {
|
|
198
|
-
if (!
|
|
235
|
+
if (!isTranslationRegisteringSource(fileManager.source)) continue;
|
|
236
|
+
sourceFiles.add(path);
|
|
199
237
|
const sourceFile = await readConfigAst(fileManager.absolutePath);
|
|
200
238
|
if (sourceFile) for (const key of extractTranslationKeys(sourceFile)) keys.add(key);
|
|
201
239
|
}
|
|
240
|
+
for (const key of await listRouteLocaleKeysForTypeGeneration()) keys.add(key);
|
|
241
|
+
this.translationSourceFiles = sourceFiles;
|
|
202
242
|
const content = `// Auto-generated by Warlock.js - DO NOT EDIT
|
|
203
|
-
// Generated from groupedTranslations calls
|
|
243
|
+
// Generated from groupedTranslations calls and route-locales JSON in discovered
|
|
244
|
+
// app source. Dynamic group names/keys cannot be resolved statically and are
|
|
245
|
+
// not included here — document those yourself.
|
|
204
246
|
|
|
205
247
|
import "@warlock.js/web";
|
|
206
248
|
|
|
@@ -459,14 +501,25 @@ ${allKeys.map((key) => ` "${key}": true;`).join("\n")}
|
|
|
459
501
|
await runTypingsGeneration(this.typingsGenerationPorts);
|
|
460
502
|
}
|
|
461
503
|
/**
|
|
462
|
-
* Regenerate typings after a batch reload — but only when the batch
|
|
463
|
-
* touched a config file,
|
|
504
|
+
* Regenerate typings after a batch reload — but only when the batch
|
|
505
|
+
* plausibly touched something the typings depend on: a config file, a file
|
|
506
|
+
* that registers translation keys, or a registering file that was deleted.
|
|
507
|
+
*
|
|
508
|
+
* A deleted file is already gone from `filesOrchestrator` by the time this
|
|
509
|
+
* runs (deletes are processed before `dev-server:batch-complete` fires), so
|
|
510
|
+
* its content can't be re-checked here — `translationSourceFiles` is the
|
|
511
|
+
* set from the last generation, kept for exactly this comparison.
|
|
464
512
|
*/
|
|
465
513
|
async executeTypingsGenerator(upcomingFiles) {
|
|
466
|
-
|
|
514
|
+
const files = filesOrchestrator.getFiles();
|
|
515
|
+
if (!(Array.from(this.translationSourceFiles).some((path) => !files.has(path)) || Array.from(new Set(upcomingFiles)).some((file) => {
|
|
467
516
|
const normalizedPath = Path.normalize(file);
|
|
468
|
-
|
|
469
|
-
|
|
517
|
+
if (normalizedPath.includes("src/config/")) return true;
|
|
518
|
+
if (isRouteLocalesJsonPath(normalizedPath)) return true;
|
|
519
|
+
if (this.translationSourceFiles.has(normalizedPath)) return true;
|
|
520
|
+
const fileManager = files.get(normalizedPath);
|
|
521
|
+
return fileManager !== void 0 && isTranslationRegisteringSource(fileManager.source);
|
|
522
|
+
}))) return;
|
|
470
523
|
await runTypingsGeneration(this.typingsGenerationPorts);
|
|
471
524
|
}
|
|
472
525
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\nimport { constants } from \"fs\";\nimport { access, readFile, writeFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport ts from \"typescript\";\nimport { warlockPath } from \"../utils\";\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\nimport { readConfigAst } from \"./read-config-ast\";\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\nimport { filesOrchestrator } from \"./files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { extractTranslationKeys } from \"./translation-type-generator\";\n\n/**\n * Typings manifest structure for tracking file hashes\n */\ntype TypingsManifest = {\n version: string;\n lastBuildTime: number;\n storage: {\n sourceHash: string;\n drivers: string[];\n } | null;\n config: Record<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >;\n};\n\n/**\n * TypeGenerator - Generates TypeScript type definitions from config files\n *\n * Parses source config files using the TypeScript Compiler API to extract\n * keys and generate module augmentation types for IDE autocomplete.\n *\n * Uses manifest-based reconciliation to only regenerate when source files change.\n */\nexport class TypeGenerator {\n /**\n * Output directory for generated typings\n */\n private outputDir = warlockPath(\"typings\");\n\n /**\n * Path to typings manifest file\n */\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\n\n /**\n * Cached manifest data\n */\n private manifest: TypingsManifest | null = null;\n\n /**\n * Cache for config type info and keys\n */\n private configCache = new Map<\n string,\n {\n sourceHash: string;\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }\n >();\n\n /**\n * Generate all framework type definitions\n *\n * Uses manifest-based reconciliation:\n * - If output files don't exist: full regeneration\n * - If files exist: only regenerate changed configs\n */\n public async generateAll(): Promise<void> {\n await this.ensureOutputDir();\n\n const storageFile = join(this.outputDir, \"storage.d.ts\");\n const configFile = join(this.outputDir, \"config.d.ts\");\n const translationsFile = join(this.outputDir, \"translations.d.ts\");\n\n const [manifestExists, storageExists, configExists, translationsExist] = await Promise.all([\n this.exists(this.manifestPath),\n this.exists(storageFile),\n this.exists(configFile),\n this.exists(translationsFile),\n ]);\n\n if (!manifestExists || !storageExists || !configExists || !translationsExist) {\n // Full regeneration (first run or files deleted)\n await this.fullGeneration();\n } else {\n // Load manifest for hash comparison\n await this.loadManifest();\n // Reconciliation: only regenerate changed files\n await this.reconcile();\n }\n\n await this.generateTranslationTypes();\n\n await this.saveManifest();\n }\n\n /**\n * Full regeneration of all type files\n */\n private async fullGeneration(): Promise<void> {\n // Generate storage types\n const storageConfigPath = await this.findConfigFile(\"storage\");\n if (storageConfigPath) {\n this.generateStorageTypes(storageConfigPath);\n }\n\n // Generate config types\n await this.generateConfigTypes();\n }\n\n /**\n * Reconcile: only regenerate changed files\n */\n private async reconcile(): Promise<void> {\n const files = filesOrchestrator.getFiles();\n let storageChanged = false;\n let configChanged = false;\n let unchangedCount = 0;\n\n // Check storage config\n for (const [path, fileManager] of files) {\n if (path.startsWith(\"src/config/storage\")) {\n const manifestEntry = this.manifest?.storage;\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n storageChanged = true;\n await this.generateStorageTypes(path);\n }\n break;\n }\n }\n\n // Check config files\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name (remove dir prefix and extension)\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n const manifestEntry = this.manifest?.config[configName];\n\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\n // File changed or new - regenerate\n configChanged = true;\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n } else {\n // Unchanged - load from manifest\n unchangedCount++;\n this.configCache.set(configName, manifestEntry);\n }\n }\n\n if (configChanged) {\n await this.writeConfigTypesFromCache();\n } else {\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\n }\n }\n\n /**\n * Generate storage driver name types\n */\n public async generateStorageTypes(configPath: string): Promise<void> {\n try {\n const driverKeys = await this.extractStorageDriverKeys(configPath);\n\n if (driverKeys.length === 0) {\n devServeLog(\"âš ï¸ No storage drivers found in config\");\n return;\n }\n\n // Get file hash from filesOrchestrator\n const fileManager = filesOrchestrator.getFiles().get(configPath);\n const sourceHash = fileManager?.hash || \"\";\n\n // Update manifest\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.storage = {\n sourceHash,\n drivers: driverKeys,\n };\n\n const interfaceContent = driverKeys\n .map((k) => ` ${this.toInterfaceKey(k)}: true;`)\n .join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from: ${configPath}\n// Regenerates on dev-server start and when storage config changes\n\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface StorageDriverRegistry {\n${interfaceContent}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"storage.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate storage types: ${error}`);\n }\n }\n\n /**\n * Check if a file change should trigger type regeneration\n */\n public shouldRegenerateTypes(changedPath: string): boolean {\n return (\n changedPath.includes(\"src/config/\") ||\n changedPath.includes(\"config/\") ||\n this.isLocalesFile(changedPath)\n );\n }\n\n /**\n * Handle file change - uses incremental update via cache\n */\n public async handleFileChange(changedPath: string): Promise<void> {\n if (!this.shouldRegenerateTypes(changedPath)) {\n return;\n }\n\n // Regenerate storage types if storage config changed\n if (changedPath.includes(\"config/storage\")) {\n await this.generateStorageTypes(changedPath);\n await this.saveManifest();\n return;\n }\n\n if (this.isLocalesFile(changedPath)) {\n await this.generateTranslationTypes();\n return;\n }\n\n // Extract config name from path\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\n if (!match) {\n return;\n }\n\n const configName = match[1];\n if (configName === \"index\") return;\n\n devLogInfo(`Config changed: ${configName}, updating...`);\n\n // Get file manager for hash\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\n const sourceHash = fileManager?.hash || Date.now().toString();\n\n // Update only the changed config in cache (use optimized combined extraction)\n const configDir = join(process.cwd(), \"src/config\");\n const configPath = join(configDir, `${configName}.ts`);\n\n const info = await this.extractConfigInfo(configPath, configName);\n\n this.configCache.set(configName, {\n sourceHash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n\n // Regenerate config.d.ts from cache\n await this.writeConfigTypesFromCache();\n await this.saveManifest();\n }\n\n /**\n * Generate config types - populates cache and writes file\n */\n public async generateConfigTypes(): Promise<void> {\n try {\n const files = filesOrchestrator.getFiles();\n\n // Clear and repopulate cache\n this.configCache.clear();\n\n for (const [path, fileManager] of files) {\n if (!path.startsWith(\"src/config/\")) continue;\n if (path.includes(\"index\")) continue;\n\n // Extract config name\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\n\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\n\n this.configCache.set(configName, {\n sourceHash: fileManager.hash,\n typeName: info.typeName,\n importSource: info.importSource,\n keys: info.keys,\n });\n }\n\n await this.writeConfigTypesFromCache();\n } catch (error) {\n devServeLog(`âš ï¸ Failed to generate config types: ${error}`);\n }\n }\n\n /** Generate web's app-augmented translation-key registry. */\n private async generateTranslationTypes(): Promise<void> {\n const keys = new Set<string>();\n\n for (const [path, fileManager] of filesOrchestrator.getFiles()) {\n if (!this.isLocalesFile(path)) {\n continue;\n }\n\n const sourceFile = await readConfigAst(fileManager.absolutePath);\n if (sourceFile) {\n for (const key of extractTranslationKeys(sourceFile)) {\n keys.add(key);\n }\n }\n }\n\n const entries = Array.from(keys)\n .sort()\n .map((key) => ` ${JSON.stringify(key)}: true;`)\n .join(\"\\n\");\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Generated from groupedTranslations calls in app locale files\n\nimport \"@warlock.js/web\";\n\ndeclare module \"@warlock.js/web\" {\n interface TranslationKeyRegistry {\n${entries}\n }\n}\n`;\n\n await writeFile(join(this.outputDir, \"translations.d.ts\"), content, \"utf-8\");\n devLogSuccess(`Generated translation types: ${keys.size} keys`);\n }\n\n private isLocalesFile(path: string): boolean {\n return Path.normalize(path).includes(\"/utils/locales.\");\n }\n\n /**\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\n * named `use-cases` or a storage driver `do-spaces`) so the generated\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\n * to keep the output clean.\n */\n private toInterfaceKey(name: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n }\n\n /**\n * Write config.d.ts from cached data\n */\n private async writeConfigTypesFromCache(): Promise<void> {\n const configTypeInfos: Array<{\n name: string;\n typeName: string | null;\n importSource: string | null;\n }> = [];\n const allKeys: string[] = [];\n\n // Update manifest config section\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.config = {};\n\n for (const [name, data] of this.configCache) {\n configTypeInfos.push({\n name,\n typeName: data.typeName,\n importSource: data.importSource,\n });\n allKeys.push(...data.keys);\n\n // Store in manifest\n this.manifest.config[name] = data;\n }\n\n // Group imports by source\n const imports = new Map<string, Set<string>>();\n for (const info of configTypeInfos) {\n if (info.typeName && info.importSource) {\n if (!imports.has(info.importSource)) {\n imports.set(info.importSource, new Set());\n }\n imports.get(info.importSource)!.add(info.typeName);\n }\n }\n\n const importStatements = Array.from(imports.entries())\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\n .join(\"\\n\");\n\n const configEntries = configTypeInfos\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\n .join(\"\\n\");\n\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\n\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\n// Regenerates on dev-server start and when config files change\n\n${importStatements}\nimport \"@warlock.js/core\";\n\ndeclare module \"@warlock.js/core\" {\n interface ConfigRegistry {\n${configEntries}\n }\n\n interface ConfigKeyRegistry {\n${keyEntries}\n }\n}\n`;\n\n const outputPath = join(this.outputDir, \"config.d.ts\");\n await writeFile(outputPath, content, \"utf-8\");\n\n devLogSuccess(\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\n );\n }\n\n // ============================================================\n // Manifest Management\n // ============================================================\n\n /**\n * Load manifest from disk\n */\n private async loadManifest(): Promise<boolean> {\n try {\n if (await this.exists(this.manifestPath)) {\n const content = await readFile(this.manifestPath, \"utf-8\");\n this.manifest = JSON.parse(content);\n return true;\n }\n } catch {\n // Manifest corrupted or missing\n }\n this.manifest = null;\n return false;\n }\n\n /**\n * Save manifest to disk\n */\n private async saveManifest(): Promise<void> {\n if (!this.manifest) {\n this.manifest = this.createEmptyManifest();\n }\n this.manifest.lastBuildTime = Date.now();\n\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\n }\n\n /**\n * Create empty manifest structure\n */\n private createEmptyManifest(): TypingsManifest {\n return {\n version: \"1.0.0\",\n lastBuildTime: Date.now(),\n storage: null,\n config: {},\n };\n }\n\n // ============================================================\n // Type Extraction Methods\n // ============================================================\n\n /**\n * Extract BOTH type info AND keys in a single pass\n *\n * One parse serves both extractions. It used to be one whole TypeScript\n * Program per file — halved from two by an earlier pass, which optimised\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\n * numbers.\n *\n * @param configPath Absolute path to the config file\n * @param configName Config name (e.g., \"auth\", \"notifications\")\n * @returns Combined result with type info and keys\n */\n private async extractConfigInfo(\n configPath: string,\n configName: string,\n ): Promise<{\n typeName: string | null;\n importSource: string | null;\n keys: string[];\n }> {\n if (!(await this.exists(configPath))) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\n const sourceFile = await readConfigAst(configPath);\n\n if (!sourceFile) {\n return { typeName: null, importSource: null, keys: [] };\n }\n\n // === Type Info Extraction ===\n const importedTypes = new Map<string, string>();\n const localExportedTypes = new Set<string>();\n let foundTypeName: string | null = null;\n\n // === Keys Extraction ===\n const keys: string[] = [];\n\n const visitForTypes = (node: ts.Node): void => {\n // Collect imported types\n if (ts.isImportDeclaration(node)) {\n const moduleSpecifier = node.moduleSpecifier;\n if (ts.isStringLiteral(moduleSpecifier)) {\n const source = moduleSpecifier.text;\n const importClause = node.importClause;\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\n for (const element of importClause.namedBindings.elements) {\n importedTypes.set(element.name.text, source);\n }\n }\n }\n }\n\n // Collect locally exported types\n if (ts.isTypeAliasDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Collect locally exported interfaces\n if (ts.isInterfaceDeclaration(node)) {\n const modifiers = ts.getModifiers(node);\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\n localExportedTypes.add(node.name.text);\n }\n }\n\n // Find type used on config variable + extract keys\n if (ts.isVariableDeclaration(node)) {\n // Type info\n if (node.type && ts.isTypeReferenceNode(node.type)) {\n foundTypeName = node.type.typeName.getText(sourceFile);\n }\n\n // Keys extraction\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\n for (const prop of objNode.properties) {\n if (ts.isPropertyAssignment(prop) && prop.name) {\n const keyName = prop.name.getText(sourceFile);\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\n keys.push(fullKey);\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n visitKeys(prop.initializer, fullKey);\n }\n }\n }\n };\n visitKeys(node.initializer, configName);\n }\n }\n\n ts.forEachChild(node, visitForTypes);\n };\n\n ts.forEachChild(sourceFile, visitForTypes);\n\n // Resolve type info\n let typeName: string | null = null;\n let importSource: string | null = null;\n\n if (foundTypeName) {\n if (importedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n importSource = importedTypes.get(foundTypeName)!;\n } else if (localExportedTypes.has(foundTypeName)) {\n typeName = foundTypeName;\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\n importSource = `../../${relativePath}`;\n }\n }\n\n return { typeName, importSource, keys };\n }\n\n /**\n * Extract driver keys from storage config\n */\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\n const absolutePath = resolve(configPath);\n\n if (!(await this.exists(absolutePath))) {\n devServeLog(`âš ï¸ Storage config not found: ${absolutePath}`);\n return [];\n }\n\n const sourceFile = await readConfigAst(absolutePath);\n\n if (!sourceFile) {\n devServeLog(`âš ï¸ Could not parse storage config: ${absolutePath}`);\n return [];\n }\n\n const keys: string[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isPropertyAssignment(node)) {\n const propName = node.name.getText(sourceFile);\n\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\n for (const prop of node.initializer.properties) {\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\n const keyName = prop.name?.getText(sourceFile);\n\n if (keyName) {\n keys.push(keyName);\n }\n }\n }\n }\n }\n\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n\n return keys;\n }\n\n /**\n * Find a config file by name\n */\n private async findConfigFile(configName: string): Promise<string | undefined> {\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\n\n for (const path of possiblePaths) {\n const fullPath = join(process.cwd(), path);\n\n if (await this.exists(fullPath)) {\n return path;\n }\n }\n\n try {\n const files = filesOrchestrator.getFiles();\n\n for (const [filePath] of files) {\n if (filePath.includes(`config/${configName}`)) {\n return filePath;\n }\n }\n } catch {\n // Files orchestrator not initialized yet\n }\n\n return undefined;\n }\n\n /**\n * Ensure output directory exists\n */\n private async ensureOutputDir(): Promise<void> {\n await ensureDirectoryAsync(this.outputDir);\n }\n\n /**\n * Check if a path exists (async wrapper)\n */\n private async exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * The ports `runTypingsGeneration` needs, bound to this instance and this\n * package's dev logger.\n */\n private get typingsGenerationPorts(): TypingsGenerationPorts {\n return {\n generate: () => this.generateAll(),\n info: (message) => devLogInfo(message),\n success: (message) => devLogSuccess(message),\n error: (message) => devLogError(message),\n };\n }\n\n /**\n * Generate every config's typings.\n *\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\n * which could not resolve in a source checkout — see\n * `run-typings-generation.ts` for the whole story.\n */\n public async executeGenerateAllCommand(): Promise<void> {\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n\n /**\n * Regenerate typings after a batch reload — but only when the batch actually\n * touched a config file, since nothing else contributes to them.\n */\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\n const touchedAConfig = Array.from(new Set(upcomingFiles)).some((file) => {\n const normalizedPath = Path.normalize(file);\n return normalizedPath.includes(\"src/config/\") || this.isLocalesFile(normalizedPath);\n });\n\n if (!touchedAConfig) return;\n\n /*\n The changed paths are deliberately NOT passed along. The previous version\n built a `files` array here and then threw it away, spawning the same\n whole-project command as the branch above — so \"incremental\" was a name,\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\n reload has already updated, so the full pass is both correct and the only\n pass that ever actually ran.\n */\n await runTypingsGeneration(this.typingsGenerationPorts);\n }\n}\n\n/**\n * Singleton instance for use throughout dev-server\n */\nexport const typeGenerator = new TypeGenerator();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"type-generator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/type-generator.ts"],"sourcesContent":["import { ensureDirectoryAsync } from \"@warlock.js/fs\";\r\nimport { constants } from \"fs\";\r\nimport { access, readFile, writeFile } from \"fs/promises\";\r\nimport { moduleResolve } from \"import-meta-resolve\";\r\nimport { join, resolve } from \"path\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport ts from \"typescript\";\r\nimport { warlockPath } from \"../utils\";\r\nimport { devLogError, devLogInfo, devLogSuccess, devServeLog } from \"./dev-logger\";\r\nimport { readConfigAst } from \"./read-config-ast\";\r\nimport { runTypingsGeneration, type TypingsGenerationPorts } from \"./run-typings-generation\";\r\nimport { filesOrchestrator } from \"./files-orchestrator\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { extractTranslationKeys } from \"./extract-translation-keys\";\r\nimport { isTranslationRegisteringSource } from \"./translation-keys-sources\";\r\n\r\ntype RouteLocaleBuildModule = {\r\n listRouteLocaleKeys?: (options: {\r\n appRoot?: string;\r\n }) => readonly string[] | Promise<readonly string[]>;\r\n};\r\n\r\nconst WEB_PACKAGE = \"@warlock.js/web\";\r\nconst WEB_BUILD_PACKAGE = \"@warlock.js/web/build\";\r\n\r\nfunction isAbsentOptionalWebPackage(thrown: unknown): boolean {\r\n return (\r\n typeof thrown === \"object\" &&\r\n thrown !== null &&\r\n \"code\" in thrown &&\r\n (thrown as { code?: unknown }).code === \"ERR_MODULE_NOT_FOUND\" &&\r\n thrown instanceof Error &&\r\n thrown.message.includes(`Cannot find package '${WEB_PACKAGE}'`)\r\n );\r\n}\r\n\r\n/** Load the build-only web key lister without creating a core-to-web static edge. */\r\nexport async function listRouteLocaleKeysForTypeGeneration(\r\n appRoot = process.cwd(),\r\n): Promise<readonly string[]> {\r\n // Published web entries are import-only. Resolve with ESM conditions from\r\n // the consuming project; require.resolve rejects those installed exports.\r\n const projectUrl = pathToFileURL(resolve(appRoot, \"package.json\"));\r\n\r\n try {\r\n moduleResolve(WEB_PACKAGE, projectUrl);\r\n } catch (thrown) {\r\n if (isAbsentOptionalWebPackage(thrown)) return [];\r\n throw thrown;\r\n }\r\n\r\n const buildModuleUrl = moduleResolve(WEB_BUILD_PACKAGE, projectUrl);\r\n const buildModule = (await import(buildModuleUrl.href)) as RouteLocaleBuildModule;\r\n\r\n if (typeof buildModule.listRouteLocaleKeys !== \"function\") {\r\n throw new Error(\r\n `${WEB_BUILD_PACKAGE} does not export listRouteLocaleKeys. Upgrade @warlock.js/web to a compatible version.`,\r\n );\r\n }\r\n\r\n return buildModule.listRouteLocaleKeys({ appRoot });\r\n}\r\n\r\n/** True only for the route-locales convention under the canonical web root. */\r\nexport function isRouteLocalesJsonPath(path: string): boolean {\r\n const normalized = Path.normalize(path);\r\n const relativePath = normalized.startsWith(\"src/\") ? normalized : Path.toRelative(normalized);\r\n return /^src\\/web(?:\\/.*)?\\/locales\\.json$/.test(relativePath);\r\n}\r\n\r\n/**\r\n * Typings manifest structure for tracking file hashes\r\n */\r\ntype TypingsManifest = {\r\n version: string;\r\n lastBuildTime: number;\r\n storage: {\r\n sourceHash: string;\r\n drivers: string[];\r\n } | null;\r\n config: Record<\r\n string,\r\n {\r\n sourceHash: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }\r\n >;\r\n};\r\n\r\n/**\r\n * TypeGenerator - Generates TypeScript type definitions from config files\r\n *\r\n * Parses source config files using the TypeScript Compiler API to extract\r\n * keys and generate module augmentation types for IDE autocomplete.\r\n *\r\n * Uses manifest-based reconciliation to only regenerate when source files change.\r\n */\r\nexport class TypeGenerator {\r\n /**\r\n * Output directory for generated typings\r\n */\r\n private outputDir = warlockPath(\"typings\");\r\n\r\n /**\r\n * Path to typings manifest file\r\n */\r\n private manifestPath = join(this.outputDir, \"typings-manifest.json\");\r\n\r\n /**\r\n * Cached manifest data\r\n */\r\n private manifest: TypingsManifest | null = null;\r\n\r\n /**\r\n * Cache for config type info and keys\r\n */\r\n private configCache = new Map<\r\n string,\r\n {\r\n sourceHash: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }\r\n >();\r\n\r\n /**\r\n * Generate all framework type definitions\r\n *\r\n * Uses manifest-based reconciliation:\r\n * - If output files don't exist: full regeneration\r\n * - If files exist: only regenerate changed configs\r\n */\r\n public async generateAll(): Promise<void> {\r\n await this.ensureOutputDir();\r\n\r\n const storageFile = join(this.outputDir, \"storage.d.ts\");\r\n const configFile = join(this.outputDir, \"config.d.ts\");\r\n const translationsFile = join(this.outputDir, \"translations.d.ts\");\r\n\r\n const [manifestExists, storageExists, configExists, translationsExist] = await Promise.all([\r\n this.exists(this.manifestPath),\r\n this.exists(storageFile),\r\n this.exists(configFile),\r\n this.exists(translationsFile),\r\n ]);\r\n\r\n if (!manifestExists || !storageExists || !configExists || !translationsExist) {\r\n // Full regeneration (first run or files deleted)\r\n await this.fullGeneration();\r\n } else {\r\n // Load manifest for hash comparison\r\n await this.loadManifest();\r\n // Reconciliation: only regenerate changed files\r\n await this.reconcile();\r\n }\r\n\r\n await this.generateTranslationTypes();\r\n\r\n await this.saveManifest();\r\n }\r\n\r\n /**\r\n * Full regeneration of all type files\r\n */\r\n private async fullGeneration(): Promise<void> {\r\n // Generate storage types\r\n const storageConfigPath = await this.findConfigFile(\"storage\");\r\n if (storageConfigPath) {\r\n this.generateStorageTypes(storageConfigPath);\r\n }\r\n\r\n // Generate config types\r\n await this.generateConfigTypes();\r\n }\r\n\r\n /**\r\n * Reconcile: only regenerate changed files\r\n */\r\n private async reconcile(): Promise<void> {\r\n const files = filesOrchestrator.getFiles();\r\n let storageChanged = false;\r\n let configChanged = false;\r\n let unchangedCount = 0;\r\n\r\n // Check storage config\r\n for (const [path, fileManager] of files) {\r\n if (path.startsWith(\"src/config/storage\")) {\r\n const manifestEntry = this.manifest?.storage;\r\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\r\n storageChanged = true;\r\n await this.generateStorageTypes(path);\r\n }\r\n break;\r\n }\r\n }\r\n\r\n // Check config files\r\n for (const [path, fileManager] of files) {\r\n if (!path.startsWith(\"src/config/\")) continue;\r\n if (path.includes(\"index\")) continue;\r\n\r\n // Extract config name (remove dir prefix and extension)\r\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\r\n\r\n const manifestEntry = this.manifest?.config[configName];\r\n\r\n if (!manifestEntry || manifestEntry.sourceHash !== fileManager.hash) {\r\n // File changed or new - regenerate\r\n configChanged = true;\r\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash: fileManager.hash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n } else {\r\n // Unchanged - load from manifest\r\n unchangedCount++;\r\n this.configCache.set(configName, manifestEntry);\r\n }\r\n }\r\n\r\n if (configChanged) {\r\n await this.writeConfigTypesFromCache();\r\n } else {\r\n devLogInfo(`Config types unchanged (${unchangedCount} configs cached)`);\r\n }\r\n }\r\n\r\n /**\r\n * Generate storage driver name types\r\n */\r\n public async generateStorageTypes(configPath: string): Promise<void> {\r\n try {\r\n const driverKeys = await this.extractStorageDriverKeys(configPath);\r\n\r\n if (driverKeys.length === 0) {\r\n devServeLog(\"âš ï¸ No storage drivers found in config\");\r\n return;\r\n }\r\n\r\n // Get file hash from filesOrchestrator\r\n const fileManager = filesOrchestrator.getFiles().get(configPath);\r\n const sourceHash = fileManager?.hash || \"\";\r\n\r\n // Update manifest\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.storage = {\r\n sourceHash,\r\n drivers: driverKeys,\r\n };\r\n\r\n const interfaceContent = driverKeys\r\n .map((k) => ` ${this.toInterfaceKey(k)}: true;`)\r\n .join(\"\\n\");\r\n\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Generated from: ${configPath}\r\n// Regenerates on dev-server start and when storage config changes\r\n\r\nimport \"@warlock.js/core\";\r\n\r\ndeclare module \"@warlock.js/core\" {\r\n interface StorageDriverRegistry {\r\n${interfaceContent}\r\n }\r\n}\r\n`;\r\n\r\n const outputPath = join(this.outputDir, \"storage.d.ts\");\r\n await writeFile(outputPath, content, \"utf-8\");\r\n\r\n devLogSuccess(`Generated storage types: ${driverKeys.join(\", \")}`);\r\n } catch (error) {\r\n devServeLog(`âš ï¸ Failed to generate storage types: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Check if a file change should trigger type regeneration\r\n */\r\n public shouldRegenerateTypes(changedPath: string): boolean {\r\n return (\r\n changedPath.includes(\"src/config/\") ||\r\n changedPath.includes(\"config/\") ||\r\n this.isLocalesFile(changedPath) ||\r\n isRouteLocalesJsonPath(changedPath)\r\n );\r\n }\r\n\r\n /**\r\n * Handle file change - uses incremental update via cache\r\n */\r\n public async handleFileChange(changedPath: string): Promise<void> {\r\n if (!this.shouldRegenerateTypes(changedPath)) {\r\n return;\r\n }\r\n\r\n // Regenerate storage types if storage config changed\r\n if (changedPath.includes(\"config/storage\")) {\r\n await this.generateStorageTypes(changedPath);\r\n await this.saveManifest();\r\n return;\r\n }\r\n\r\n if (this.isLocalesFile(changedPath) || isRouteLocalesJsonPath(changedPath)) {\r\n await this.generateTranslationTypes();\r\n return;\r\n }\r\n\r\n // Extract config name from path\r\n const match = changedPath.match(/config\\/([^/]+)\\.[^.]+$/);\r\n if (!match) {\r\n return;\r\n }\r\n\r\n const configName = match[1];\r\n if (configName === \"index\") return;\r\n\r\n devLogInfo(`Config changed: ${configName}, updating...`);\r\n\r\n // Get file manager for hash\r\n const fileManager = filesOrchestrator.getFiles().get(changedPath);\r\n const sourceHash = fileManager?.hash || Date.now().toString();\r\n\r\n // Update only the changed config in cache (use optimized combined extraction)\r\n const configDir = join(process.cwd(), \"src/config\");\r\n const configPath = join(configDir, `${configName}.ts`);\r\n\r\n const info = await this.extractConfigInfo(configPath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n\r\n // Regenerate config.d.ts from cache\r\n await this.writeConfigTypesFromCache();\r\n await this.saveManifest();\r\n }\r\n\r\n /**\r\n * Generate config types - populates cache and writes file\r\n */\r\n public async generateConfigTypes(): Promise<void> {\r\n try {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n // Clear and repopulate cache\r\n this.configCache.clear();\r\n\r\n for (const [path, fileManager] of files) {\r\n if (!path.startsWith(\"src/config/\")) continue;\r\n if (path.includes(\"index\")) continue;\r\n\r\n // Extract config name\r\n const configName = path.replace(\"src/config/\", \"\").replace(/\\.[^.]+$/, \"\");\r\n\r\n // One parse per config, both extractions off it — see `read-config-ast.ts`.\r\n const info = await this.extractConfigInfo(fileManager.absolutePath, configName);\r\n\r\n this.configCache.set(configName, {\r\n sourceHash: fileManager.hash,\r\n typeName: info.typeName,\r\n importSource: info.importSource,\r\n keys: info.keys,\r\n });\r\n }\r\n\r\n await this.writeConfigTypesFromCache();\r\n } catch (error) {\r\n devServeLog(`âš ï¸ Failed to generate config types: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Files (relative paths) that contributed to the last generated registry —\r\n * used to notice a registering file being deleted, since by the time a\r\n * delete event reaches {@link executeTypingsGenerator} the file is already\r\n * gone from `filesOrchestrator`'s map and its content can no longer be\r\n * inspected.\r\n */\r\n private translationSourceFiles = new Set<string>();\r\n\r\n /**\r\n * Generate web's app-augmented translation-key registry.\r\n *\r\n * Scans every discovered app source file — not only dedicated\r\n * `utils/locales.*` files — for `groupedTranslations` calls, both the\r\n * named-group and object forms, at any nesting depth. Dynamic group names,\r\n * dictionaries, or keys cannot be resolved statically and are skipped; an\r\n * app relying on those must document them itself.\r\n */\r\n private async generateTranslationTypes(): Promise<void> {\r\n const keys = new Set<string>();\r\n const sourceFiles = new Set<string>();\r\n\r\n for (const [path, fileManager] of filesOrchestrator.getFiles()) {\r\n if (!isTranslationRegisteringSource(fileManager.source)) {\r\n continue;\r\n }\r\n\r\n sourceFiles.add(path);\r\n\r\n const sourceFile = await readConfigAst(fileManager.absolutePath);\r\n if (sourceFile) {\r\n for (const key of extractTranslationKeys(sourceFile)) {\r\n keys.add(key);\r\n }\r\n }\r\n }\r\n\r\n for (const key of await listRouteLocaleKeysForTypeGeneration()) {\r\n keys.add(key);\r\n }\r\n\r\n this.translationSourceFiles = sourceFiles;\r\n\r\n const entries = Array.from(keys)\r\n .sort()\r\n .map((key) => ` ${JSON.stringify(key)}: true;`)\r\n .join(\"\\n\");\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Generated from groupedTranslations calls and route-locales JSON in discovered\r\n// app source. Dynamic group names/keys cannot be resolved statically and are\r\n// not included here — document those yourself.\r\n\r\nimport \"@warlock.js/web\";\r\n\r\ndeclare module \"@warlock.js/web\" {\r\n interface TranslationKeyRegistry {\r\n${entries}\r\n }\r\n}\r\n`;\r\n\r\n await writeFile(join(this.outputDir, \"translations.d.ts\"), content, \"utf-8\");\r\n devLogSuccess(`Generated translation types: ${keys.size} keys`);\r\n }\r\n\r\n private isLocalesFile(path: string): boolean {\r\n return Path.normalize(path).includes(\"/utils/locales.\");\r\n }\r\n\r\n /**\r\n * Quote an interface key that isn't a valid JS identifier (e.g. a config\r\n * named `use-cases` or a storage driver `do-spaces`) so the generated\r\n * `.d.ts` stays syntactically valid. Identifier-safe names are left bare\r\n * to keep the output clean.\r\n */\r\n private toInterfaceKey(name: string): string {\r\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\r\n }\r\n\r\n /**\r\n * Write config.d.ts from cached data\r\n */\r\n private async writeConfigTypesFromCache(): Promise<void> {\r\n const configTypeInfos: Array<{\r\n name: string;\r\n typeName: string | null;\r\n importSource: string | null;\r\n }> = [];\r\n const allKeys: string[] = [];\r\n\r\n // Update manifest config section\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.config = {};\r\n\r\n for (const [name, data] of this.configCache) {\r\n configTypeInfos.push({\r\n name,\r\n typeName: data.typeName,\r\n importSource: data.importSource,\r\n });\r\n allKeys.push(...data.keys);\r\n\r\n // Store in manifest\r\n this.manifest.config[name] = data;\r\n }\r\n\r\n // Group imports by source\r\n const imports = new Map<string, Set<string>>();\r\n for (const info of configTypeInfos) {\r\n if (info.typeName && info.importSource) {\r\n if (!imports.has(info.importSource)) {\r\n imports.set(info.importSource, new Set());\r\n }\r\n imports.get(info.importSource)!.add(info.typeName);\r\n }\r\n }\r\n\r\n const importStatements = Array.from(imports.entries())\r\n .map(([source, types]) => `import type { ${Array.from(types).join(\", \")} } from \"${source}\";`)\r\n .join(\"\\n\");\r\n\r\n const configEntries = configTypeInfos\r\n .map((info) => ` ${this.toInterfaceKey(info.name)}: ${info.typeName || \"unknown\"};`)\r\n .join(\"\\n\");\r\n\r\n const keyEntries = allKeys.map((key) => ` \"${key}\": true;`).join(\"\\n\");\r\n\r\n const content = `// Auto-generated by Warlock.js - DO NOT EDIT\r\n// Regenerates on dev-server start and when config files change\r\n\r\n${importStatements}\r\nimport \"@warlock.js/core\";\r\n\r\ndeclare module \"@warlock.js/core\" {\r\n interface ConfigRegistry {\r\n${configEntries}\r\n }\r\n\r\n interface ConfigKeyRegistry {\r\n${keyEntries}\r\n }\r\n}\r\n`;\r\n\r\n const outputPath = join(this.outputDir, \"config.d.ts\");\r\n await writeFile(outputPath, content, \"utf-8\");\r\n\r\n devLogSuccess(\r\n `Generated config types: ${this.configCache.size} configs, ${allKeys.length} keys`,\r\n );\r\n }\r\n\r\n // ============================================================\r\n // Manifest Management\r\n // ============================================================\r\n\r\n /**\r\n * Load manifest from disk\r\n */\r\n private async loadManifest(): Promise<boolean> {\r\n try {\r\n if (await this.exists(this.manifestPath)) {\r\n const content = await readFile(this.manifestPath, \"utf-8\");\r\n this.manifest = JSON.parse(content);\r\n return true;\r\n }\r\n } catch {\r\n // Manifest corrupted or missing\r\n }\r\n this.manifest = null;\r\n return false;\r\n }\r\n\r\n /**\r\n * Save manifest to disk\r\n */\r\n private async saveManifest(): Promise<void> {\r\n if (!this.manifest) {\r\n this.manifest = this.createEmptyManifest();\r\n }\r\n this.manifest.lastBuildTime = Date.now();\r\n\r\n await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2), \"utf-8\");\r\n }\r\n\r\n /**\r\n * Create empty manifest structure\r\n */\r\n private createEmptyManifest(): TypingsManifest {\r\n return {\r\n version: \"1.0.0\",\r\n lastBuildTime: Date.now(),\r\n storage: null,\r\n config: {},\r\n };\r\n }\r\n\r\n // ============================================================\r\n // Type Extraction Methods\r\n // ============================================================\r\n\r\n /**\r\n * Extract BOTH type info AND keys in a single pass\r\n *\r\n * One parse serves both extractions. It used to be one whole TypeScript\r\n * Program per file — halved from two by an earlier pass, which optimised\r\n * inside a premise that did not need to hold. `read-config-ast.ts` has the\r\n * numbers.\r\n *\r\n * @param configPath Absolute path to the config file\r\n * @param configName Config name (e.g., \"auth\", \"notifications\")\r\n * @returns Combined result with type info and keys\r\n */\r\n private async extractConfigInfo(\r\n configPath: string,\r\n configName: string,\r\n ): Promise<{\r\n typeName: string | null;\r\n importSource: string | null;\r\n keys: string[];\r\n }> {\r\n if (!(await this.exists(configPath))) {\r\n return { typeName: null, importSource: null, keys: [] };\r\n }\r\n\r\n // Parse, do not resolve — `read-config-ast.ts` explains why, with numbers.\r\n const sourceFile = await readConfigAst(configPath);\r\n\r\n if (!sourceFile) {\r\n return { typeName: null, importSource: null, keys: [] };\r\n }\r\n\r\n // === Type Info Extraction ===\r\n const importedTypes = new Map<string, string>();\r\n const localExportedTypes = new Set<string>();\r\n let foundTypeName: string | null = null;\r\n\r\n // === Keys Extraction ===\r\n const keys: string[] = [];\r\n\r\n const visitForTypes = (node: ts.Node): void => {\r\n // Collect imported types\r\n if (ts.isImportDeclaration(node)) {\r\n const moduleSpecifier = node.moduleSpecifier;\r\n if (ts.isStringLiteral(moduleSpecifier)) {\r\n const source = moduleSpecifier.text;\r\n const importClause = node.importClause;\r\n if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) {\r\n for (const element of importClause.namedBindings.elements) {\r\n importedTypes.set(element.name.text, source);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Collect locally exported types\r\n if (ts.isTypeAliasDeclaration(node)) {\r\n const modifiers = ts.getModifiers(node);\r\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\r\n localExportedTypes.add(node.name.text);\r\n }\r\n }\r\n\r\n // Collect locally exported interfaces\r\n if (ts.isInterfaceDeclaration(node)) {\r\n const modifiers = ts.getModifiers(node);\r\n if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {\r\n localExportedTypes.add(node.name.text);\r\n }\r\n }\r\n\r\n // Find type used on config variable + extract keys\r\n if (ts.isVariableDeclaration(node)) {\r\n // Type info\r\n if (node.type && ts.isTypeReferenceNode(node.type)) {\r\n foundTypeName = node.type.typeName.getText(sourceFile);\r\n }\r\n\r\n // Keys extraction\r\n if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) {\r\n const visitKeys = (objNode: ts.ObjectLiteralExpression, prefix: string): void => {\r\n for (const prop of objNode.properties) {\r\n if (ts.isPropertyAssignment(prop) && prop.name) {\r\n const keyName = prop.name.getText(sourceFile);\r\n const fullKey = prefix ? `${prefix}.${keyName}` : keyName;\r\n keys.push(fullKey);\r\n if (ts.isObjectLiteralExpression(prop.initializer)) {\r\n visitKeys(prop.initializer, fullKey);\r\n }\r\n }\r\n }\r\n };\r\n visitKeys(node.initializer, configName);\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visitForTypes);\r\n };\r\n\r\n ts.forEachChild(sourceFile, visitForTypes);\r\n\r\n // Resolve type info\r\n let typeName: string | null = null;\r\n let importSource: string | null = null;\r\n\r\n if (foundTypeName) {\r\n if (importedTypes.has(foundTypeName)) {\r\n typeName = foundTypeName;\r\n importSource = importedTypes.get(foundTypeName)!;\r\n } else if (localExportedTypes.has(foundTypeName)) {\r\n typeName = foundTypeName;\r\n const relativePath = Path.toRelative(configPath).replace(/\\.(ts|tsx)$/, \"\");\r\n importSource = `../../${relativePath}`;\r\n }\r\n }\r\n\r\n return { typeName, importSource, keys };\r\n }\r\n\r\n /**\r\n * Extract driver keys from storage config\r\n */\r\n private async extractStorageDriverKeys(configPath: string): Promise<string[]> {\r\n const absolutePath = resolve(configPath);\r\n\r\n if (!(await this.exists(absolutePath))) {\r\n devServeLog(`âš ï¸ Storage config not found: ${absolutePath}`);\r\n return [];\r\n }\r\n\r\n const sourceFile = await readConfigAst(absolutePath);\r\n\r\n if (!sourceFile) {\r\n devServeLog(`âš ï¸ Could not parse storage config: ${absolutePath}`);\r\n return [];\r\n }\r\n\r\n const keys: string[] = [];\r\n\r\n const visit = (node: ts.Node): void => {\r\n if (ts.isPropertyAssignment(node)) {\r\n const propName = node.name.getText(sourceFile);\r\n\r\n if (propName === \"drivers\" && ts.isObjectLiteralExpression(node.initializer)) {\r\n for (const prop of node.initializer.properties) {\r\n if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {\r\n const keyName = prop.name?.getText(sourceFile);\r\n\r\n if (keyName) {\r\n keys.push(keyName);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n ts.forEachChild(node, visit);\r\n };\r\n\r\n ts.forEachChild(sourceFile, visit);\r\n\r\n return keys;\r\n }\r\n\r\n /**\r\n * Find a config file by name\r\n */\r\n private async findConfigFile(configName: string): Promise<string | undefined> {\r\n const possiblePaths = [`src/config/${configName}.ts`, `config/${configName}.ts`];\r\n\r\n for (const path of possiblePaths) {\r\n const fullPath = join(process.cwd(), path);\r\n\r\n if (await this.exists(fullPath)) {\r\n return path;\r\n }\r\n }\r\n\r\n try {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n for (const [filePath] of files) {\r\n if (filePath.includes(`config/${configName}`)) {\r\n return filePath;\r\n }\r\n }\r\n } catch {\r\n // Files orchestrator not initialized yet\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Ensure output directory exists\r\n */\r\n private async ensureOutputDir(): Promise<void> {\r\n await ensureDirectoryAsync(this.outputDir);\r\n }\r\n\r\n /**\r\n * Check if a path exists (async wrapper)\r\n */\r\n private async exists(path: string): Promise<boolean> {\r\n try {\r\n await access(path, constants.F_OK);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * The ports `runTypingsGeneration` needs, bound to this instance and this\r\n * package's dev logger.\r\n */\r\n private get typingsGenerationPorts(): TypingsGenerationPorts {\r\n return {\r\n generate: () => this.generateAll(),\r\n info: (message) => devLogInfo(message),\r\n success: (message) => devLogSuccess(message),\r\n error: (message) => devLogError(message),\r\n };\r\n }\r\n\r\n /**\r\n * Generate every config's typings.\r\n *\r\n * Runs IN THIS PROCESS. It used to spawn `npx warlock generate.typings`,\r\n * which could not resolve in a source checkout — see\r\n * `run-typings-generation.ts` for the whole story.\r\n */\r\n public async executeGenerateAllCommand(): Promise<void> {\r\n await runTypingsGeneration(this.typingsGenerationPorts);\r\n }\r\n\r\n /**\r\n * Regenerate typings after a batch reload — but only when the batch\r\n * plausibly touched something the typings depend on: a config file, a file\r\n * that registers translation keys, or a registering file that was deleted.\r\n *\r\n * A deleted file is already gone from `filesOrchestrator` by the time this\r\n * runs (deletes are processed before `dev-server:batch-complete` fires), so\r\n * its content can't be re-checked here — `translationSourceFiles` is the\r\n * set from the last generation, kept for exactly this comparison.\r\n */\r\n public async executeTypingsGenerator(upcomingFiles: string[]): Promise<void> {\r\n const files = filesOrchestrator.getFiles();\r\n\r\n const aKnownTranslationSourceVanished = Array.from(this.translationSourceFiles).some(\r\n (path) => !files.has(path),\r\n );\r\n\r\n const touchedAConfig =\r\n aKnownTranslationSourceVanished ||\r\n Array.from(new Set(upcomingFiles)).some((file) => {\r\n const normalizedPath = Path.normalize(file);\r\n if (normalizedPath.includes(\"src/config/\")) return true;\r\n if (isRouteLocalesJsonPath(normalizedPath)) return true;\r\n if (this.translationSourceFiles.has(normalizedPath)) return true;\r\n\r\n const fileManager = files.get(normalizedPath);\r\n return fileManager !== undefined && isTranslationRegisteringSource(fileManager.source);\r\n });\r\n\r\n if (!touchedAConfig) return;\r\n\r\n /*\r\n The changed paths are deliberately NOT passed along. The previous version\r\n built a `files` array here and then threw it away, spawning the same\r\n whole-project command as the branch above — so \"incremental\" was a name,\r\n not a behaviour. `generateAll()` reads the orchestrator, which the batch\r\n reload has already updated, so the full pass is both correct and the only\r\n pass that ever actually ran.\r\n */\r\n await runTypingsGeneration(this.typingsGenerationPorts);\r\n }\r\n}\r\n\r\n/**\r\n * Singleton instance for use throughout dev-server\r\n */\r\nexport const typeGenerator = new TypeGenerator();\r\n"],"mappings":"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"}
|