From 74adeee75a6cdb290ab6127fb94281c7582e3b46 Mon Sep 17 00:00:00 2001 From: Tanner Reits <47483144+tanner-reits@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:08:27 -0400 Subject: [PATCH] fix(compiler): respect project tsconfig watch options (#5916) * fix(compiler): respect project tsconfig watch options Fixes: #5709 STENCIL-1079 * add tests * export function --- src/compiler/config/load-config.ts | 1 + .../tests/typescript-config.spec.ts | 65 +++++++++++++++++-- .../sys/typescript/typescript-config.ts | 7 +- .../transpile/create-watch-program.ts | 9 +++ .../test/create-watch-program.spec.ts | 46 +++++++++++++ src/declarations/stencil-public-compiler.ts | 1 + 6 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 src/compiler/transpile/test/create-watch-program.spec.ts diff --git a/src/compiler/config/load-config.ts b/src/compiler/config/load-config.ts index 2729c8e1611..b69cff10ea3 100644 --- a/src/compiler/config/load-config.ts +++ b/src/compiler/config/load-config.ts @@ -82,6 +82,7 @@ export const loadConfig = async (init: LoadConfigInit = {}): Promise { describe('hasSrcDirectoryInclude', () => { it('returns `false` for a non-array argument', () => { // the intent of this test is to evaluate when a user doesn't provide an array, hence the type assertion - expect(hasSrcDirectoryInclude('src' as unknown as string[], 'src')).toBe(false); + expect(tsConfig.hasSrcDirectoryInclude('src' as unknown as string[], 'src')).toBe(false); }); it('returns `false` for an empty array', () => { - expect(hasSrcDirectoryInclude([], 'src/')).toBe(false); + expect(tsConfig.hasSrcDirectoryInclude([], 'src/')).toBe(false); }); it('returns `false` when an entry does not exist in the array', () => { - expect(hasSrcDirectoryInclude(['src'], 'source')).toBe(false); + expect(tsConfig.hasSrcDirectoryInclude(['src'], 'source')).toBe(false); }); it('returns `true` when an entry does exist in the array', () => { - expect(hasSrcDirectoryInclude(['src', 'foo'], 'src')).toBe(true); + expect(tsConfig.hasSrcDirectoryInclude(['src', 'foo'], 'src')).toBe(true); }); it('returns `true` for globs', () => { - expect(hasSrcDirectoryInclude(['src/**/*.ts', 'foo/'], 'src/**/*.ts')).toBe(true); + expect(tsConfig.hasSrcDirectoryInclude(['src/**/*.ts', 'foo/'], 'src/**/*.ts')).toBe(true); }); it.each([ @@ -29,7 +34,53 @@ describe('typescript-config', () => { [['../src'], '../src'], [['*'], './*'], ])('returns `true` for relative paths', (includedPaths, srcDir) => { - expect(hasSrcDirectoryInclude(includedPaths, srcDir)).toBe(true); + expect(tsConfig.hasSrcDirectoryInclude(includedPaths, srcDir)).toBe(true); + }); + }); + + describe('validateTsConfig', () => { + let mockSys: TestingSystem; + let config: ValidatedConfig; + let tsSpy: jest.SpyInstance; + + beforeEach(() => { + mockSys = createTestingSystem(); + config = mockValidatedConfig(); + + jest.spyOn(tsConfig, 'getTsConfigPath').mockResolvedValue({ + path: 'tsconfig.json', + content: '', + }); + tsSpy = jest.spyOn(ts, 'getParsedCommandLineOfConfigFile'); + }); + + it('includes watchOptions when provided', async () => { + tsSpy.mockReturnValueOnce({ + watchOptions: { + excludeFiles: ['exclude.ts'], + excludeDirectories: ['exclude-dir'], + }, + options: null, + fileNames: [], + errors: [], + }); + + const result = await tsConfig.validateTsConfig(config, mockSys, {}); + expect(result.watchOptions).toEqual({ + excludeFiles: ['exclude.ts'], + excludeDirectories: ['exclude-dir'], + }); + }); + + it('does not include watchOptions when not provided', async () => { + tsSpy.mockReturnValueOnce({ + options: null, + fileNames: [], + errors: [], + }); + + const result = await tsConfig.validateTsConfig(config, mockSys, {}); + expect(result.watchOptions).toEqual({}); }); }); }); diff --git a/src/compiler/sys/typescript/typescript-config.ts b/src/compiler/sys/typescript/typescript-config.ts index 55c20c10b8a..b8ffad87a0d 100644 --- a/src/compiler/sys/typescript/typescript-config.ts +++ b/src/compiler/sys/typescript/typescript-config.ts @@ -17,6 +17,7 @@ export const validateTsConfig = async (config: d.ValidatedConfig, sys: d.Compile const tsconfig = { path: '', compilerOptions: {} as ts.CompilerOptions, + watchOptions: {} as ts.WatchOptions, files: [] as string[], include: [] as string[], exclude: [] as string[], @@ -91,6 +92,10 @@ export const validateTsConfig = async (config: d.ValidatedConfig, sys: d.Compile } } + if (results.watchOptions) { + tsconfig.watchOptions = results.watchOptions; + } + if (results.options) { tsconfig.compilerOptions = results.options; @@ -119,7 +124,7 @@ export const validateTsConfig = async (config: d.ValidatedConfig, sys: d.Compile return tsconfig; }; -const getTsConfigPath = async ( +export const getTsConfigPath = async ( config: d.ValidatedConfig, sys: d.CompilerSystem, init: d.LoadConfigInit, diff --git a/src/compiler/transpile/create-watch-program.ts b/src/compiler/transpile/create-watch-program.ts index 4f9da3bef7f..1d7c942af59 100644 --- a/src/compiler/transpile/create-watch-program.ts +++ b/src/compiler/transpile/create-watch-program.ts @@ -82,6 +82,15 @@ export const createTsWatchProgram = async ( (reportWatchStatus) => { config.logger.debug(reportWatchStatus.messageText); }, + // We don't want to allow users to mess with the watch method, so + // we only strip out the excludeFiles and excludeDirectories properties + // to allow the user to still have control over which files get excluded from the watcher + config.tsWatchOptions + ? { + excludeFiles: config.tsWatchOptions.excludeFiles, + excludeDirectories: config.tsWatchOptions.excludeDirectories, + } + : undefined, ); // Add a callback that will execute whenever a new instance diff --git a/src/compiler/transpile/test/create-watch-program.spec.ts b/src/compiler/transpile/test/create-watch-program.spec.ts new file mode 100644 index 00000000000..6fc7f8e24dc --- /dev/null +++ b/src/compiler/transpile/test/create-watch-program.spec.ts @@ -0,0 +1,46 @@ +import ts from 'typescript'; + +import { ValidatedConfig } from '../../../declarations'; +import { mockValidatedConfig } from '../../../testing/mocks'; +import { createTsWatchProgram } from '../create-watch-program'; + +describe('createWatchProgram', () => { + let config: ValidatedConfig; + + beforeEach(() => { + config = mockValidatedConfig(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('includes watchOptions in the watch program creation', async () => { + config.tsWatchOptions = { + fallbackPolling: 3, + excludeFiles: ['src/components/my-component/my-component.tsx'], + excludeDirectories: ['src/components/my-other-component'], + } as ts.WatchOptions; + config.tsconfig = ''; + const tsSpy = jest.spyOn(ts, 'createWatchCompilerHost').mockReturnValue({} as any); + jest.spyOn(ts, 'createWatchProgram').mockReturnValue({} as any); + + await createTsWatchProgram(config, () => new Promise(() => {})); + + expect(tsSpy.mock.calls[0][6]).toEqual({ + excludeFiles: ['src/components/my-component/my-component.tsx'], + excludeDirectories: ['src/components/my-other-component'], + }); + }); + + it('omits watchOptions when not provided', async () => { + config.tsWatchOptions = undefined; + config.tsconfig = ''; + const tsSpy = jest.spyOn(ts, 'createWatchCompilerHost').mockReturnValue({} as any); + jest.spyOn(ts, 'createWatchProgram').mockReturnValue({} as any); + + await createTsWatchProgram(config, () => new Promise(() => {})); + + expect(tsSpy.mock.calls[0][6]).toEqual(undefined); + }); +}); diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 3ef6dc6daf0..cc0251f5dcd 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -428,6 +428,7 @@ export interface Config extends StencilConfig { suppressLogs?: boolean; profile?: boolean; tsCompilerOptions?: any; + tsWatchOptions?: any; _isValidated?: boolean; _isTesting?: boolean; }