From 2f884b13ba91d096f3b4ca954b7c5045b8bc0568 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Wed, 27 Aug 2025 17:19:56 -0700 Subject: [PATCH 1/7] Support .gjs.d.ts --- packages/core/src/common/transform-manager.ts | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/core/src/common/transform-manager.ts b/packages/core/src/common/transform-manager.ts index 1ee6b5c82..c9530fad4 100644 --- a/packages/core/src/common/transform-manager.ts +++ b/packages/core/src/common/transform-manager.ts @@ -287,7 +287,36 @@ export default class TransformManager { }; public fileExists = (filename: string): boolean => { - return this.documents.documentExists(filename); + // First check if the file exists normally + if (this.documents.documentExists(filename)) { + return true; + } + + // If it's a .d.ts file that doesn't exist, check for declaration files + // with the original extension (e.g. x.gjs.d.ts for x.d.ts when x.gjs exists) + if (filename.endsWith('.d.ts')) { + const baseName = filename.slice(0, -5); // Remove '.d.ts' + const possibleSourceExtensions = [ + ...this.glintConfig.environment.typedScriptExtensions, + ...this.glintConfig.environment.untypedScriptExtensions, + ]; + + for (const sourceExt of possibleSourceExtensions) { + if (sourceExt !== '.ts' && sourceExt !== '.js') { + // Check if there's a source file with this extension + const sourceFile = baseName + sourceExt; + if (this.documents.documentExists(sourceFile)) { + // Check if there's a declaration file for the original extension + const originalDtsFile = sourceFile + '.d.ts'; + if (this.ts.sys.fileExists(originalDtsFile)) { + return true; + } + } + } + } + } + + return false; }; public readTransformedFile = (filename: string, encoding?: string): string | undefined => { @@ -295,6 +324,30 @@ export default class TransformManager { if (transformInfo?.transformedModule) { return transformInfo.transformedModule.transformedContents; } else { + // Check if this is a request for a declaration file that should be served + // from the original extension (e.g. serve x.gjs.d.ts when x.d.ts is requested) + if (filename.endsWith('.d.ts')) { + const baseName = filename.slice(0, -5); // Remove '.d.ts' + const possibleSourceExtensions = [ + ...this.glintConfig.environment.typedScriptExtensions, + ...this.glintConfig.environment.untypedScriptExtensions, + ]; + + for (const sourceExt of possibleSourceExtensions) { + if (sourceExt !== '.ts' && sourceExt !== '.js') { + // Check if there's a source file with this extension + const sourceFile = baseName + sourceExt; + if (this.documents.documentExists(sourceFile)) { + // Check if there's a declaration file for the original extension + const originalDtsFile = sourceFile + '.d.ts'; + if (this.ts.sys.fileExists(originalDtsFile)) { + return this.ts.sys.readFile(originalDtsFile, encoding); + } + } + } + } + } + return this.documents.getDocumentContents(filename, encoding); } }; From 4afea0ddd89ff599fd7a713ca7eda89b14a2268c Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 10:32:50 -0700 Subject: [PATCH 2/7] Clean up more --- packages/core/src/common/transform-manager.ts | 76 ++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/packages/core/src/common/transform-manager.ts b/packages/core/src/common/transform-manager.ts index c9530fad4..1f018a12b 100644 --- a/packages/core/src/common/transform-manager.ts +++ b/packages/core/src/common/transform-manager.ts @@ -292,31 +292,8 @@ export default class TransformManager { return true; } - // If it's a .d.ts file that doesn't exist, check for declaration files - // with the original extension (e.g. x.gjs.d.ts for x.d.ts when x.gjs exists) - if (filename.endsWith('.d.ts')) { - const baseName = filename.slice(0, -5); // Remove '.d.ts' - const possibleSourceExtensions = [ - ...this.glintConfig.environment.typedScriptExtensions, - ...this.glintConfig.environment.untypedScriptExtensions, - ]; - - for (const sourceExt of possibleSourceExtensions) { - if (sourceExt !== '.ts' && sourceExt !== '.js') { - // Check if there's a source file with this extension - const sourceFile = baseName + sourceExt; - if (this.documents.documentExists(sourceFile)) { - // Check if there's a declaration file for the original extension - const originalDtsFile = sourceFile + '.d.ts'; - if (this.ts.sys.fileExists(originalDtsFile)) { - return true; - } - } - } - } - } - - return false; + // Check for declaration files with alternative extensions + return this.findAlternativeDeclarationFile(filename) !== null; }; public readTransformedFile = (filename: string, encoding?: string): string | undefined => { @@ -327,24 +304,9 @@ export default class TransformManager { // Check if this is a request for a declaration file that should be served // from the original extension (e.g. serve x.gjs.d.ts when x.d.ts is requested) if (filename.endsWith('.d.ts')) { - const baseName = filename.slice(0, -5); // Remove '.d.ts' - const possibleSourceExtensions = [ - ...this.glintConfig.environment.typedScriptExtensions, - ...this.glintConfig.environment.untypedScriptExtensions, - ]; - - for (const sourceExt of possibleSourceExtensions) { - if (sourceExt !== '.ts' && sourceExt !== '.js') { - // Check if there's a source file with this extension - const sourceFile = baseName + sourceExt; - if (this.documents.documentExists(sourceFile)) { - // Check if there's a declaration file for the original extension - const originalDtsFile = sourceFile + '.d.ts'; - if (this.ts.sys.fileExists(originalDtsFile)) { - return this.ts.sys.readFile(originalDtsFile, encoding); - } - } - } + const alternativeFile = this.findAlternativeDeclarationFile(filename); + if (alternativeFile) { + return this.ts.sys.readFile(alternativeFile, encoding); } } @@ -522,6 +484,34 @@ export default class TransformManager { ) ); } + + private findAlternativeDeclarationFile(filename: string): string | null { + if (!filename.endsWith('.d.ts')) { + return null; + } + + const baseName = filename.slice(0, -5); // Remove '.d.ts' + const possibleSourceExtensions = [ + ...this.glintConfig.environment.typedScriptExtensions, + ...this.glintConfig.environment.untypedScriptExtensions, + ]; + + for (const sourceExt of possibleSourceExtensions) { + if (sourceExt !== '.ts' && sourceExt !== '.js') { + // Check if there's a source file with this extension + const sourceFile = baseName + sourceExt; + if (this.documents.documentExists(sourceFile)) { + // Check if there's a declaration file for the original extension + const originalDtsFile = sourceFile + '.d.ts'; + if (this.ts.sys.fileExists(originalDtsFile)) { + return originalDtsFile; + } + } + } + } + + return null; + } } function statSync(path: string): Stats | undefined { From 0a59de1be469cd52d25e6d17c73960cd798809f2 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 11:08:08 -0700 Subject: [PATCH 3/7] Add tests --- .../core/__tests__/transform-manager.test.ts | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 packages/core/__tests__/transform-manager.test.ts diff --git a/packages/core/__tests__/transform-manager.test.ts b/packages/core/__tests__/transform-manager.test.ts new file mode 100644 index 000000000..ad3f7171c --- /dev/null +++ b/packages/core/__tests__/transform-manager.test.ts @@ -0,0 +1,409 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import TransformManager from '../src/common/transform-manager.js'; + +/** + * Tests for TransformManager's file existence and reading functionality, + * specifically testing the new feature that supports finding .gjs.d.ts and .gts.d.ts + * files when looking for .d.ts files (for Glint V1 to V2 migration support). + * + * This supports the scenario where: + * - A .gjs or .gts file exists (e.g., component.gjs) + * - A corresponding declaration file exists (e.g., component.gjs.d.ts) + * - Code imports using .d.ts extension (e.g., import from './component.d.ts') + * - TransformManager should find and use the .gjs.d.ts file + */ + +describe('TransformManager', () => { + let mockTS: any; + let mockGlintConfig: any; + let mockDocumentCache: any; + let transformManager: TransformManager; + + beforeEach(() => { + mockTS = { + sys: { + fileExists: vi.fn(), + readFile: vi.fn(), + getCurrentDirectory: vi.fn(() => '/test'), + }, + createModuleResolutionCache: vi.fn(() => ({})), + sortAndDeduplicateDiagnostics: vi.fn((diags: any) => diags), + }; + + mockGlintConfig = { + ts: mockTS, + environment: { + typedScriptExtensions: ['.ts', '.gts', '.gjs'], + untypedScriptExtensions: ['.js'], + }, + }; + + mockDocumentCache = { + documentExists: vi.fn(), + getDocumentContents: vi.fn(), + }; + + transformManager = new TransformManager(mockGlintConfig, mockDocumentCache); + }); + + describe('fileExists', () => { + test('returns true if document exists normally', () => { + mockDocumentCache.documentExists.mockReturnValue(true); + + const result = transformManager.fileExists('/path/to/file.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/file.ts'); + }); + + test('returns false for non-.d.ts files that do not exist normally', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + + const result = transformManager.fileExists('/path/to/file.ts'); + + expect(result).toBe(false); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/file.ts'); + }); + + describe('for .d.ts files', () => { + test('returns false if no alternative declaration file exists', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + mockTS.sys.fileExists.mockReturnValue(false); + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + }); + + test('returns true if .gts source file exists and has corresponding .gts.d.ts', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + }); + + test('returns true if .gjs source file exists and has corresponding .gjs.d.ts', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(false) // component.gts doesn't exist (checked first) + .mockReturnValueOnce(true); // component.gjs exists (checked second) + mockTS.sys.fileExists.mockReturnValue(true); // component.gjs.d.ts exists + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gts'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gjs'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + + test('returns false if .gts source file exists but no .gts.d.ts file', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + }); + + test('skips .ts and .js extensions when looking for alternatives', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + mockTS.sys.fileExists.mockReturnValue(false); + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.ts'); + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.js'); + }); + + test('checks multiple extensions until one is found', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gjs.d.ts exists + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gts'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gjs'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + + test('stops checking after finding first match', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(true); // component.gts exists (first match) + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + // Should not check .gjs since .gts was found first + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.gjs'); + }); + }); + }); + + describe('readTransformedFile', () => { + beforeEach(() => { + // Mock getTransformInfo to return null (no transform info) + vi.spyOn(transformManager as any, 'getTransformInfo').mockReturnValue(null); + }); + + test('returns document contents for non-.d.ts files', () => { + mockDocumentCache.getDocumentContents.mockReturnValue('file contents'); + + const result = transformManager.readTransformedFile('/path/to/file.ts', 'utf8'); + + expect(result).toBe('file contents'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/file.ts', 'utf8'); + }); + + test('returns document contents for .d.ts files with no alternatives', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + mockTS.sys.fileExists.mockReturnValue(false); + mockDocumentCache.getDocumentContents.mockReturnValue('fallback contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('fallback contents'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/component.d.ts', 'utf8'); + }); + + describe('for .d.ts files with alternatives', () => { + test('returns contents from .gts.d.ts when .gts source exists', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + mockTS.sys.readFile.mockReturnValue('declaration file contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('declaration file contents'); + expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.gts.d.ts', 'utf8'); + }); + + test('returns contents from .gjs.d.ts when .gjs source exists', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gjs.d.ts exists + mockTS.sys.readFile.mockReturnValue('gjs declaration contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('gjs declaration contents'); + expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.gjs.d.ts', 'utf8'); + }); + + test('falls back to document contents if alternative exists but no .d.ts file', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist + mockDocumentCache.getDocumentContents.mockReturnValue('fallback contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('fallback contents'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/component.d.ts', 'utf8'); + }); + + test('skips .ts and .js extensions when looking for alternatives', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + mockTS.sys.fileExists.mockReturnValue(false); + mockDocumentCache.getDocumentContents.mockReturnValue('fallback contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('fallback contents'); + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.ts'); + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.js'); + }); + + test('stops at first matching alternative file', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(true); // component.gts exists (first match) + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + mockTS.sys.readFile.mockReturnValue('gts declaration contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('gts declaration contents'); + // Should not check .gjs since .gts was found first + expect(mockDocumentCache.documentExists).not.toHaveBeenCalledWith('/path/to/component.gjs'); + }); + }); + + test('returns transformed contents when transform info exists', () => { + const mockTransformInfo = { + transformedModule: { + transformedContents: 'transformed contents' + } + }; + vi.spyOn(transformManager as any, 'getTransformInfo').mockReturnValue(mockTransformInfo); + + const result = transformManager.readTransformedFile('/path/to/file.ts', 'utf8'); + + expect(result).toBe('transformed contents'); + }); + }); + + describe('findAlternativeDeclarationFile (private method)', () => { + test('returns null for non-.d.ts files', () => { + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/file.ts'); + + expect(result).toBeNull(); + }); + + test('returns null when no alternative source files exist', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + mockTS.sys.fileExists.mockReturnValue(false); + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBeNull(); + }); + + test('returns .gts.d.ts path when .gts source exists', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBe('/path/to/component.gts.d.ts'); + }); + + test('returns .gjs.d.ts path when .gjs source exists', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gjs.d.ts exists + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBe('/path/to/component.gjs.d.ts'); + }); + + test('returns null when source exists but no corresponding .d.ts file', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBeNull(); + }); + }); + + describe('integration with different file extensions', () => { + test('handles complex file paths correctly', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // normal check fails + .mockReturnValueOnce(true); // .gts exists + mockTS.sys.fileExists.mockReturnValue(true); + + const result = transformManager.fileExists('/deep/nested/path/to/my-component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/deep/nested/path/to/my-component.gts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/deep/nested/path/to/my-component.gts.d.ts'); + }); + + test('handles files with multiple dots in name', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // my.special.component.d.ts doesn't exist normally + .mockReturnValueOnce(false) // my.special.component.gts doesn't exist (checked first) + .mockReturnValueOnce(true); // my.special.component.gjs exists (checked second) + mockTS.sys.fileExists.mockReturnValue(true); + + const result = transformManager.fileExists('/path/to/my.special.component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/my.special.component.gts'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/my.special.component.gjs'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/my.special.component.gjs.d.ts'); + }); + + test('handles empty alternative extensions list', () => { + // Override the mock config to have no alternative extensions + mockGlintConfig.environment = { + typedScriptExtensions: ['.ts'], + untypedScriptExtensions: ['.js'], + }; + + // Re-create transform manager with new config + transformManager = new TransformManager(mockGlintConfig, mockDocumentCache); + + mockDocumentCache.documentExists.mockReturnValue(false); + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + // Should only check the normal existence, no alternative checks + expect(mockDocumentCache.documentExists).toHaveBeenCalledTimes(1); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.d.ts'); + }); + + test('handles readTransformedFile with encoding parameter', () => { + vi.spyOn(transformManager as any, 'getTransformInfo').mockReturnValue(null); + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + mockTS.sys.readFile.mockReturnValue('utf16 encoded contents'); + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf16'); + + expect(result).toBe('utf16 encoded contents'); + expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.gts.d.ts', 'utf16'); + }); + + test('readTransformedFile returns undefined when file system returns undefined', () => { + vi.spyOn(transformManager as any, 'getTransformInfo').mockReturnValue(null); + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists + mockTS.sys.readFile.mockReturnValue(undefined); // File read returns undefined + + const result = transformManager.readTransformedFile('/path/to/component.d.ts'); + + expect(result).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + test('fileExists handles very short filenames', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + + const result = transformManager.fileExists('a.d.ts'); + + expect(result).toBe(false); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('a.d.ts'); + }); + + test('fileExists handles filenames that are exactly .d.ts', () => { + mockDocumentCache.documentExists.mockReturnValue(false); + + const result = transformManager.fileExists('.d.ts'); + + expect(result).toBe(false); + // Should check for alternatives with empty base name + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('.d.ts'); + }); + + test('findAlternativeDeclarationFile handles case where source exists but different extension .d.ts does not', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBeNull(); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + }); + }); +}); From adc200f9bdd4fc7375fcf2ba4645b21410ccdd69 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 13:16:18 -0700 Subject: [PATCH 4/7] More improvements --- .../core/__tests__/transform-manager.test.ts | 244 +++++++++++++++++- packages/core/src/common/transform-manager.ts | 20 +- packages/core/src/config/config.ts | 29 +++ 3 files changed, 285 insertions(+), 8 deletions(-) diff --git a/packages/core/__tests__/transform-manager.test.ts b/packages/core/__tests__/transform-manager.test.ts index ad3f7171c..543002446 100644 --- a/packages/core/__tests__/transform-manager.test.ts +++ b/packages/core/__tests__/transform-manager.test.ts @@ -3,14 +3,20 @@ import TransformManager from '../src/common/transform-manager.js'; /** * Tests for TransformManager's file existence and reading functionality, - * specifically testing the new feature that supports finding .gjs.d.ts and .gts.d.ts - * files when looking for .d.ts files (for Glint V1 to V2 migration support). + * specifically testing the feature that supports finding .gjs.d.ts, .gts.d.ts, + * .d.gjs.ts, and .d.gts.ts files when looking for .d.ts files (for Glint V1 to V2 migration support). * * This supports the scenario where: * - A .gjs or .gts file exists (e.g., component.gjs) - * - A corresponding declaration file exists (e.g., component.gjs.d.ts) + * - A corresponding declaration file exists in one of two patterns: + * - Standard: component.gjs.d.ts (always supported) + * - Arbitrary extensions: component.d.gjs.ts (when allowArbitraryExtensions is enabled) * - Code imports using .d.ts extension (e.g., import from './component.d.ts') - * - TransformManager should find and use the .gjs.d.ts file + * - TransformManager should find and use the appropriate declaration file + * + * The implementation respects TypeScript's allowArbitraryExtensions compiler option: + * - When true: checks both .gjs.d.ts and .d.gjs.ts patterns + * - When false (default): only checks .gjs.d.ts pattern */ describe('TransformManager', () => { @@ -36,6 +42,7 @@ describe('TransformManager', () => { typedScriptExtensions: ['.ts', '.gts', '.gjs'], untypedScriptExtensions: ['.js'], }, + getCompilerOptions: vi.fn().mockReturnValue({}), // Default empty options (allowArbitraryExtensions: undefined) }; mockDocumentCache = { @@ -103,6 +110,89 @@ describe('TransformManager', () => { expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); }); + test('returns true if .gts source file exists and has corresponding .d.gts.ts (arbitrary extensions)', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(true); // component.gts exists + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gts.ts exists + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.d.gts.ts'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns true if .gjs source file exists and has corresponding .d.gjs.ts (arbitrary extensions)', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(false) // component.gts doesn't exist (checked first) + .mockReturnValueOnce(true); // component.gjs exists (checked second) + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gjs.ts exists + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(true); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gts'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/component.gjs'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.d.gjs.ts'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns false if .gts source file exists but allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should only check for standard pattern (.gts.d.ts) for the .gts source found + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(1); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + }); + + test('returns false if .gjs source file exists but allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.d.ts doesn't exist normally + .mockReturnValueOnce(false) // component.gts doesn't exist (checked first) + .mockReturnValueOnce(true); // component.gjs exists (checked second) + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.fileExists('/path/to/component.d.ts'); + + expect(result).toBe(false); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should only check for standard pattern, not arbitrary pattern + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(1); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + test('returns false if .gts source file exists but no .gts.d.ts file', () => { mockDocumentCache.documentExists .mockReturnValueOnce(false) // component.d.ts doesn't exist normally @@ -206,6 +296,81 @@ describe('TransformManager', () => { expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.gjs.d.ts', 'utf8'); }); + test('returns contents from .d.gts.ts when .gts source exists, standard pattern not found, and allowArbitraryExtensions is enabled', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gts.ts exists + mockTS.sys.readFile.mockReturnValue('arbitrary extensions declaration contents'); + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('arbitrary extensions declaration contents'); + expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.d.gts.ts', 'utf8'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns contents from .d.gjs.ts when .gjs source exists, standard pattern not found, and allowArbitraryExtensions is enabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gjs.ts exists + mockTS.sys.readFile.mockReturnValue('arbitrary gjs declaration contents'); + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBe('arbitrary gjs declaration contents'); + expect(mockTS.sys.readFile).toHaveBeenCalledWith('/path/to/component.d.gjs.ts', 'utf8'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns undefined when .gts source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBeUndefined(); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should check for both .gts and .gjs standard patterns, but not arbitrary patterns + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(2); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + + test('returns undefined when .gjs source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); + + expect(result).toBeUndefined(); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should only check for standard pattern, not arbitrary pattern + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(1); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + test('falls back to document contents if alternative exists but no .d.ts file', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist @@ -293,6 +458,77 @@ describe('TransformManager', () => { expect(result).toBe('/path/to/component.gjs.d.ts'); }); + test('returns .d.gts.ts path when .gts source exists, standard pattern not found, and allowArbitraryExtensions is enabled', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gts.ts exists + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBe('/path/to/component.d.gts.ts'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns .d.gjs.ts path when .gjs source exists, standard pattern not found, and allowArbitraryExtensions is enabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists + .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist + .mockReturnValueOnce(true); // component.d.gjs.ts exists + + // Mock getCompilerOptions to return allowArbitraryExtensions: true + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBe('/path/to/component.d.gjs.ts'); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + }); + + test('returns null when .gjs source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists + .mockReturnValueOnce(false) // component.gts doesn't exist + .mockReturnValueOnce(true); // component.gjs exists + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBeNull(); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should only check for standard pattern, not arbitrary pattern + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(1); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + + test('returns null when .gts source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { + mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists + mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist + + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) + const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); + mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; + + const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + + expect(result).toBeNull(); + expect(mockGetCompilerOptions).toHaveBeenCalled(); + // Should check standard patterns for both .gts and .gjs extensions (iterates through all) + expect(mockTS.sys.fileExists).toHaveBeenCalledTimes(2); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gjs.d.ts'); + }); + test('returns null when source exists but no corresponding .d.ts file', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist diff --git a/packages/core/src/common/transform-manager.ts b/packages/core/src/common/transform-manager.ts index 1f018a12b..c09f2ad70 100644 --- a/packages/core/src/common/transform-manager.ts +++ b/packages/core/src/common/transform-manager.ts @@ -501,10 +501,22 @@ export default class TransformManager { // Check if there's a source file with this extension const sourceFile = baseName + sourceExt; if (this.documents.documentExists(sourceFile)) { - // Check if there's a declaration file for the original extension - const originalDtsFile = sourceFile + '.d.ts'; - if (this.ts.sys.fileExists(originalDtsFile)) { - return originalDtsFile; + // Check for both .ext.d.ts and .d.ext.ts patterns + // Pattern 1: component.gjs.d.ts (standard TypeScript pattern) + const standardDtsFile = sourceFile + '.d.ts'; + if (this.ts.sys.fileExists(standardDtsFile)) { + return standardDtsFile; + } + + // Pattern 2: component.d.gjs.ts (allowArbitraryExtensions pattern) + // Only check this pattern if allowArbitraryExtensions is enabled + const compilerOptions = this.glintConfig.getCompilerOptions(); + // @ts-ignore: allowArbitraryExtensions may not be available in older TypeScript versions + if (compilerOptions.allowArbitraryExtensions === true) { + const arbitraryDtsFile = baseName + '.d' + sourceExt + '.ts'; + if (this.ts.sys.fileExists(arbitraryDtsFile)) { + return arbitraryDtsFile; + } } } } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f46329669..029077a12 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -14,6 +14,7 @@ export class GlintConfig { public readonly checkStandaloneTemplates: boolean; private extensions: Array; + private parsedTsConfig?: import('typescript').ParsedCommandLine; public constructor( ts: typeof import('typescript'), @@ -52,6 +53,34 @@ export class GlintConfig { return filename; } } + + /** + * Parses and returns the TypeScript configuration for this project. + * Results are cached after the first call. + */ + public getParsedTsConfig(): import('typescript').ParsedCommandLine { + if (!this.parsedTsConfig) { + const contents = this.ts.readConfigFile(this.configPath, this.ts.sys.readFile).config; + const host = { ...this.ts.sys }; + + this.parsedTsConfig = this.ts.parseJsonConfigFileContent( + contents, + host, + this.rootDir, + undefined, + this.configPath + ); + } + + return this.parsedTsConfig; + } + + /** + * Returns the TypeScript compiler options for this project. + */ + public getCompilerOptions(): import('typescript').CompilerOptions { + return this.getParsedTsConfig().options; + } } export function normalizePath(fileName: string): string { From c8ffedf719c76db49510f5112c64e31853666073 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 13:17:58 -0700 Subject: [PATCH 5/7] Add another test --- .../transform-manager-integration.test.ts | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 packages/core/__tests__/transform-manager-integration.test.ts diff --git a/packages/core/__tests__/transform-manager-integration.test.ts b/packages/core/__tests__/transform-manager-integration.test.ts new file mode 100644 index 000000000..edf4e608a --- /dev/null +++ b/packages/core/__tests__/transform-manager-integration.test.ts @@ -0,0 +1,203 @@ +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import { Project } from 'glint-monorepo-test-utils'; + +describe('TransformManager Integration: .gjs/.gts declaration files', () => { + let project!: Project; + + beforeEach(async () => { + project = await Project.createExact({ + glint: { + environment: 'ember-template-imports', + }, + compilerOptions: { + allowJs: true, + declaration: true, + emitDeclarationOnly: true, + }, + }); + }); + + afterEach(async () => { + await project.destroy(); + }); + + test('can find .gts.d.ts when importing .gts component as .d.ts', async () => { + // Create a .gts component + project.write('my-component.gts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string }; + } + + export default class MyComponent extends Component { + + } + `); + + // Manually create a .gts.d.ts file (simulating what would be generated) + project.write('my-component.gts.d.ts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string }; + } + + export default class MyComponent extends Component {} + `); + + // Create a file that tries to import the component using .d.ts extension + project.write('app.gts', ` + import Component from '@glimmer/component'; + import MyComponent from './my-component.d.ts'; + + export default class App extends Component { + + } + `); + + // Run typechecking - this should succeed without errors + let result = project.check(); + expect(await result).toMatchObject({ exitCode: 0 }); + }); + + test('can find .gjs.d.ts when importing .gjs component as .d.ts', async () => { + // Create a .gjs component + project.write('my-component.gjs', ` + import Component from '@glimmer/component'; + + export default class MyComponent extends Component { + static template = hbs\`

{{@title}}

\`; + } + `); + + // Manually create a .gjs.d.ts file (simulating what would be generated) + project.write('my-component.gjs.d.ts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string }; + } + + export default class MyComponent extends Component {} + `); + + // Create a file that tries to import the component using .d.ts extension + project.write('app.gts', ` + import Component from '@glimmer/component'; + import MyComponent from './my-component.d.ts'; + + export default class App extends Component { + + } + `); + + // Run typechecking - this should succeed without errors + let result = project.check(); + expect(await result).toMatchObject({ exitCode: 0 }); + }); + + test('falls back to normal behavior when no alternative .d.ts exists', async () => { + // Create a .gts component + project.write('my-component.gts', ` + import Component from '@glimmer/component'; + + export default class MyComponent extends Component { + + } + `); + + // Create a regular .d.ts file (not .gts.d.ts) + project.write('my-component.d.ts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string }; + } + + export default class MyComponent extends Component {} + `); + + // Create a file that imports using .d.ts extension + project.write('app.gts', ` + import Component from '@glimmer/component'; + import MyComponent from './my-component.d.ts'; + + export default class App extends Component { + + } + `); + + // This should still work, using the regular .d.ts file + let result = project.check(); + expect(await result).toMatchObject({ exitCode: 0 }); + }); + + test('prefers .gts.d.ts over .gjs.d.ts when both exist', async () => { + // Create both .gts and .gjs components with the same base name + project.write('my-component.gts', ` + import Component from '@glimmer/component'; + + export default class MyComponent extends Component { + + } + `); + + project.write('my-component.gjs', ` + import Component from '@glimmer/component'; + + export default class MyComponent extends Component { + static template = hbs\`

GJS: {{@title}}

\`; + } + `); + + // Create both declaration files with slightly different signatures + project.write('my-component.gts.d.ts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string; gtsSpecific?: boolean }; + } + + export default class MyComponent extends Component {} + `); + + project.write('my-component.gjs.d.ts', ` + import Component from '@glimmer/component'; + + export interface MyComponentSignature { + Args: { title: string; gjsSpecific?: boolean }; + } + + export default class MyComponent extends Component {} + `); + + // Create a file that imports using .d.ts extension and uses the gts-specific prop + project.write('app.gts', ` + import Component from '@glimmer/component'; + import MyComponent from './my-component.d.ts'; + + export default class App extends Component { + + } + `); + + // This should succeed if .gts.d.ts is preferred (gtsSpecific prop should be recognized) + let result = project.check(); + expect(await result).toMatchObject({ exitCode: 0 }); + }); +}); From 4a7175c0c4db48594d3ae244d011c1c288769e55 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 13:23:04 -0700 Subject: [PATCH 6/7] Remove integration test incompatible with v1.x The integration test depends on v2-specific test utilities that are not available in the v1.x branch. The comprehensive unit tests in transform-manager.test.ts provide sufficient coverage. --- .../transform-manager-integration.test.ts | 203 ------------------ 1 file changed, 203 deletions(-) delete mode 100644 packages/core/__tests__/transform-manager-integration.test.ts diff --git a/packages/core/__tests__/transform-manager-integration.test.ts b/packages/core/__tests__/transform-manager-integration.test.ts deleted file mode 100644 index edf4e608a..000000000 --- a/packages/core/__tests__/transform-manager-integration.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import { Project } from 'glint-monorepo-test-utils'; - -describe('TransformManager Integration: .gjs/.gts declaration files', () => { - let project!: Project; - - beforeEach(async () => { - project = await Project.createExact({ - glint: { - environment: 'ember-template-imports', - }, - compilerOptions: { - allowJs: true, - declaration: true, - emitDeclarationOnly: true, - }, - }); - }); - - afterEach(async () => { - await project.destroy(); - }); - - test('can find .gts.d.ts when importing .gts component as .d.ts', async () => { - // Create a .gts component - project.write('my-component.gts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string }; - } - - export default class MyComponent extends Component { - - } - `); - - // Manually create a .gts.d.ts file (simulating what would be generated) - project.write('my-component.gts.d.ts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string }; - } - - export default class MyComponent extends Component {} - `); - - // Create a file that tries to import the component using .d.ts extension - project.write('app.gts', ` - import Component from '@glimmer/component'; - import MyComponent from './my-component.d.ts'; - - export default class App extends Component { - - } - `); - - // Run typechecking - this should succeed without errors - let result = project.check(); - expect(await result).toMatchObject({ exitCode: 0 }); - }); - - test('can find .gjs.d.ts when importing .gjs component as .d.ts', async () => { - // Create a .gjs component - project.write('my-component.gjs', ` - import Component from '@glimmer/component'; - - export default class MyComponent extends Component { - static template = hbs\`

{{@title}}

\`; - } - `); - - // Manually create a .gjs.d.ts file (simulating what would be generated) - project.write('my-component.gjs.d.ts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string }; - } - - export default class MyComponent extends Component {} - `); - - // Create a file that tries to import the component using .d.ts extension - project.write('app.gts', ` - import Component from '@glimmer/component'; - import MyComponent from './my-component.d.ts'; - - export default class App extends Component { - - } - `); - - // Run typechecking - this should succeed without errors - let result = project.check(); - expect(await result).toMatchObject({ exitCode: 0 }); - }); - - test('falls back to normal behavior when no alternative .d.ts exists', async () => { - // Create a .gts component - project.write('my-component.gts', ` - import Component from '@glimmer/component'; - - export default class MyComponent extends Component { - - } - `); - - // Create a regular .d.ts file (not .gts.d.ts) - project.write('my-component.d.ts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string }; - } - - export default class MyComponent extends Component {} - `); - - // Create a file that imports using .d.ts extension - project.write('app.gts', ` - import Component from '@glimmer/component'; - import MyComponent from './my-component.d.ts'; - - export default class App extends Component { - - } - `); - - // This should still work, using the regular .d.ts file - let result = project.check(); - expect(await result).toMatchObject({ exitCode: 0 }); - }); - - test('prefers .gts.d.ts over .gjs.d.ts when both exist', async () => { - // Create both .gts and .gjs components with the same base name - project.write('my-component.gts', ` - import Component from '@glimmer/component'; - - export default class MyComponent extends Component { - - } - `); - - project.write('my-component.gjs', ` - import Component from '@glimmer/component'; - - export default class MyComponent extends Component { - static template = hbs\`

GJS: {{@title}}

\`; - } - `); - - // Create both declaration files with slightly different signatures - project.write('my-component.gts.d.ts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string; gtsSpecific?: boolean }; - } - - export default class MyComponent extends Component {} - `); - - project.write('my-component.gjs.d.ts', ` - import Component from '@glimmer/component'; - - export interface MyComponentSignature { - Args: { title: string; gjsSpecific?: boolean }; - } - - export default class MyComponent extends Component {} - `); - - // Create a file that imports using .d.ts extension and uses the gts-specific prop - project.write('app.gts', ` - import Component from '@glimmer/component'; - import MyComponent from './my-component.d.ts'; - - export default class App extends Component { - - } - `); - - // This should succeed if .gts.d.ts is preferred (gtsSpecific prop should be recognized) - let result = project.check(); - expect(await result).toMatchObject({ exitCode: 0 }); - }); -}); From e5e6ab9a9b2527ea42dcad5835aa304ccc813ded Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 29 Aug 2025 13:27:59 -0700 Subject: [PATCH 7/7] Lint fix --- .../core/__tests__/transform-manager.test.ts | 106 ++++++++++++------ packages/core/src/common/transform-manager.ts | 2 +- packages/core/src/config/config.ts | 4 +- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/packages/core/__tests__/transform-manager.test.ts b/packages/core/__tests__/transform-manager.test.ts index 543002446..aa80af79b 100644 --- a/packages/core/__tests__/transform-manager.test.ts +++ b/packages/core/__tests__/transform-manager.test.ts @@ -13,9 +13,9 @@ import TransformManager from '../src/common/transform-manager.js'; * - Arbitrary extensions: component.d.gjs.ts (when allowArbitraryExtensions is enabled) * - Code imports using .d.ts extension (e.g., import from './component.d.ts') * - TransformManager should find and use the appropriate declaration file - * + * * The implementation respects TypeScript's allowArbitraryExtensions compiler option: - * - When true: checks both .gjs.d.ts and .d.gjs.ts patterns + * - When true: checks both .gjs.d.ts and .d.gjs.ts patterns * - When false (default): only checks .gjs.d.ts pattern */ @@ -117,7 +117,7 @@ describe('TransformManager', () => { mockTS.sys.fileExists .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gts.ts exists - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -139,7 +139,7 @@ describe('TransformManager', () => { mockTS.sys.fileExists .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gjs.ts exists - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -159,7 +159,7 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.d.ts doesn't exist normally .mockReturnValueOnce(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -179,7 +179,7 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.gts doesn't exist (checked first) .mockReturnValueOnce(true); // component.gjs exists (checked second) mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -257,7 +257,10 @@ describe('TransformManager', () => { const result = transformManager.readTransformedFile('/path/to/file.ts', 'utf8'); expect(result).toBe('file contents'); - expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/file.ts', 'utf8'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith( + '/path/to/file.ts', + 'utf8' + ); }); test('returns document contents for .d.ts files with no alternatives', () => { @@ -268,7 +271,10 @@ describe('TransformManager', () => { const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); expect(result).toBe('fallback contents'); - expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/component.d.ts', 'utf8'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith( + '/path/to/component.d.ts', + 'utf8' + ); }); describe('for .d.ts files with alternatives', () => { @@ -302,7 +308,7 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gts.ts exists mockTS.sys.readFile.mockReturnValue('arbitrary extensions declaration contents'); - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -322,7 +328,7 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gjs.ts exists mockTS.sys.readFile.mockReturnValue('arbitrary gjs declaration contents'); - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -337,7 +343,7 @@ describe('TransformManager', () => { test('returns undefined when .gts source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -357,7 +363,7 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.gts doesn't exist .mockReturnValueOnce(true); // component.gjs exists mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; @@ -379,7 +385,10 @@ describe('TransformManager', () => { const result = transformManager.readTransformedFile('/path/to/component.d.ts', 'utf8'); expect(result).toBe('fallback contents'); - expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith('/path/to/component.d.ts', 'utf8'); + expect(mockDocumentCache.getDocumentContents).toHaveBeenCalledWith( + '/path/to/component.d.ts', + 'utf8' + ); }); test('skips .ts and .js extensions when looking for alternatives', () => { @@ -395,8 +404,7 @@ describe('TransformManager', () => { }); test('stops at first matching alternative file', () => { - mockDocumentCache.documentExists - .mockReturnValueOnce(true); // component.gts exists (first match) + mockDocumentCache.documentExists.mockReturnValueOnce(true); // component.gts exists (first match) mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists mockTS.sys.readFile.mockReturnValue('gts declaration contents'); @@ -411,8 +419,8 @@ describe('TransformManager', () => { test('returns transformed contents when transform info exists', () => { const mockTransformInfo = { transformedModule: { - transformedContents: 'transformed contents' - } + transformedContents: 'transformed contents', + }, }; vi.spyOn(transformManager as any, 'getTransformInfo').mockReturnValue(mockTransformInfo); @@ -433,7 +441,9 @@ describe('TransformManager', () => { mockDocumentCache.documentExists.mockReturnValue(false); mockTS.sys.fileExists.mockReturnValue(false); - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBeNull(); }); @@ -442,7 +452,9 @@ describe('TransformManager', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(true); // component.gts.d.ts exists - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBe('/path/to/component.gts.d.ts'); }); @@ -453,7 +465,9 @@ describe('TransformManager', () => { .mockReturnValueOnce(true); // component.gjs exists mockTS.sys.fileExists.mockReturnValue(true); // component.gjs.d.ts exists - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBe('/path/to/component.gjs.d.ts'); }); @@ -463,12 +477,14 @@ describe('TransformManager', () => { mockTS.sys.fileExists .mockReturnValueOnce(false) // component.gts.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gts.ts exists - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBe('/path/to/component.d.gts.ts'); expect(mockGetCompilerOptions).toHaveBeenCalled(); @@ -481,12 +497,14 @@ describe('TransformManager', () => { mockTS.sys.fileExists .mockReturnValueOnce(false) // component.gjs.d.ts doesn't exist .mockReturnValueOnce(true); // component.d.gjs.ts exists - + // Mock getCompilerOptions to return allowArbitraryExtensions: true const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: true }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBe('/path/to/component.d.gjs.ts'); expect(mockGetCompilerOptions).toHaveBeenCalled(); @@ -497,12 +515,14 @@ describe('TransformManager', () => { .mockReturnValueOnce(false) // component.gts doesn't exist .mockReturnValueOnce(true); // component.gjs exists mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBeNull(); expect(mockGetCompilerOptions).toHaveBeenCalled(); @@ -514,12 +534,14 @@ describe('TransformManager', () => { test('returns null when .gts source exists, standard pattern not found, and allowArbitraryExtensions is disabled', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // no .d.ts files exist - + // Mock getCompilerOptions to return allowArbitraryExtensions: false (default) const mockGetCompilerOptions = vi.fn().mockReturnValue({ allowArbitraryExtensions: false }); mockGlintConfig.getCompilerOptions = mockGetCompilerOptions; - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBeNull(); expect(mockGetCompilerOptions).toHaveBeenCalled(); @@ -533,7 +555,9 @@ describe('TransformManager', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBeNull(); }); @@ -549,8 +573,12 @@ describe('TransformManager', () => { const result = transformManager.fileExists('/deep/nested/path/to/my-component.d.ts'); expect(result).toBe(true); - expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/deep/nested/path/to/my-component.gts'); - expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/deep/nested/path/to/my-component.gts.d.ts'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith( + '/deep/nested/path/to/my-component.gts' + ); + expect(mockTS.sys.fileExists).toHaveBeenCalledWith( + '/deep/nested/path/to/my-component.gts.d.ts' + ); }); test('handles files with multiple dots in name', () => { @@ -563,8 +591,12 @@ describe('TransformManager', () => { const result = transformManager.fileExists('/path/to/my.special.component.d.ts'); expect(result).toBe(true); - expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/my.special.component.gts'); - expect(mockDocumentCache.documentExists).toHaveBeenCalledWith('/path/to/my.special.component.gjs'); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith( + '/path/to/my.special.component.gts' + ); + expect(mockDocumentCache.documentExists).toHaveBeenCalledWith( + '/path/to/my.special.component.gjs' + ); expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/my.special.component.gjs.d.ts'); }); @@ -574,10 +606,10 @@ describe('TransformManager', () => { typedScriptExtensions: ['.ts'], untypedScriptExtensions: ['.js'], }; - + // Re-create transform manager with new config transformManager = new TransformManager(mockGlintConfig, mockDocumentCache); - + mockDocumentCache.documentExists.mockReturnValue(false); const result = transformManager.fileExists('/path/to/component.d.ts'); @@ -636,7 +668,9 @@ describe('TransformManager', () => { mockDocumentCache.documentExists.mockReturnValue(true); // component.gts exists mockTS.sys.fileExists.mockReturnValue(false); // component.gts.d.ts doesn't exist - const result = (transformManager as any).findAlternativeDeclarationFile('/path/to/component.d.ts'); + const result = (transformManager as any).findAlternativeDeclarationFile( + '/path/to/component.d.ts' + ); expect(result).toBeNull(); expect(mockTS.sys.fileExists).toHaveBeenCalledWith('/path/to/component.gts.d.ts'); diff --git a/packages/core/src/common/transform-manager.ts b/packages/core/src/common/transform-manager.ts index c09f2ad70..7278b5420 100644 --- a/packages/core/src/common/transform-manager.ts +++ b/packages/core/src/common/transform-manager.ts @@ -507,7 +507,7 @@ export default class TransformManager { if (this.ts.sys.fileExists(standardDtsFile)) { return standardDtsFile; } - + // Pattern 2: component.d.gjs.ts (allowArbitraryExtensions pattern) // Only check this pattern if allowArbitraryExtensions is enabled const compilerOptions = this.glintConfig.getCompilerOptions(); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 029077a12..46c743021 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -62,7 +62,7 @@ export class GlintConfig { if (!this.parsedTsConfig) { const contents = this.ts.readConfigFile(this.configPath, this.ts.sys.readFile).config; const host = { ...this.ts.sys }; - + this.parsedTsConfig = this.ts.parseJsonConfigFileContent( contents, host, @@ -71,7 +71,7 @@ export class GlintConfig { this.configPath ); } - + return this.parsedTsConfig; }