From 4961e942d7b50f5646827308fec6296b4b0aa2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Wed, 20 Aug 2025 13:53:45 +0200 Subject: [PATCH 1/8] feature: add support for single file type checking --- docs-v2/getting-started.md | 29 ++++- docs/getting-started.md | 29 ++++- packages/core/src/cli/run-volar-tsc.ts | 87 +++++++++++++-- packages/core/src/config/index.ts | 2 +- packages/core/src/config/loader.ts | 49 ++++++++ .../__tests__/cli/single-file.test.ts | 105 ++++++++++++++++++ 6 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 test-packages/package-test-core/__tests__/cli/single-file.test.ts diff --git a/docs-v2/getting-started.md b/docs-v2/getting-started.md index 2d513eb64..dd9b17da4 100644 --- a/docs-v2/getting-started.md +++ b/docs-v2/getting-started.md @@ -51,6 +51,33 @@ To this: You can also use the `glint` command locally with the `--watch` flag to monitor your project as you work! +#### Single File Checking + +Glint supports checking individual files or a specific set of files instead of your entire project. This can be useful for faster feedback during development or when working with large codebases. + +```bash +# Check a single file +npx glint src/components/my-component.gts + +# Check multiple files +npx glint src/components/header.gts src/components/footer.gts + +# Check files with different extensions +npx glint src/helpers/format-date.ts src/components/date-picker.gts +``` + +When checking specific files, Glint: +- Uses your project's `tsconfig.json` configuration +- Applies the same type checking rules as project-wide checking +- Only analyzes the specified files for faster performance +- Maintains all your project's compiler options and path mappings + +This is particularly useful for: +- **IDE integrations** that need to check files on save +- **Git hooks** that validate only changed files +- **Development workflows** where you want quick feedback on specific components +- **CI optimizations** for incremental builds + ### Glint Editor Extensions You can install an editor extension to display Glint's diagnostics inline in your templates and provide richer editor support—typechecking, type information on hover, automated refactoring, and more—powered by `glint-language-server`: @@ -66,4 +93,4 @@ To get Ember/Glimmer and TypeScript working together, Glint creates a separate T 1. Click the little gear icon of "TypeScript and JavaScript Language Features", and select "Disable (Workspace)". 1. Reload the workspace. Glint will now take over TS language services. -![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) \ No newline at end of file +![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) diff --git a/docs/getting-started.md b/docs/getting-started.md index 58d39438a..71f70d103 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -36,6 +36,33 @@ To this: You can also use the `glint` command locally with the `--watch` flag to monitor your project as you work! +#### Single File Checking + +Glint supports checking individual files or a specific set of files instead of your entire project. This can be useful for faster feedback during development or when working with large codebases. + +```bash +# Check a single file +npx glint src/components/my-component.gts + +# Check multiple files +npx glint src/components/header.gts src/components/footer.gts + +# Check files with different extensions +npx glint src/helpers/format-date.ts src/components/date-picker.gts +``` + +When checking specific files, Glint: +- Uses your project's `tsconfig.json` configuration +- Applies the same type checking rules as project-wide checking +- Only analyzes the specified files for faster performance +- Maintains all your project's compiler options and path mappings + +This is particularly useful for: +- **IDE integrations** that need to check files on save +- **Git hooks** that validate only changed files +- **Development workflows** where you want quick feedback on specific components +- **CI optimizations** for incremental builds + ### Glint Editor Extensions You can install an editor extension to display Glint's diagnostics inline in your templates and provide richer editor support—typechecking, type information on hover, automated refactoring, and more—powered by `glint-language-server`: @@ -51,4 +78,4 @@ To get Ember/Glimmer and TypeScript working together, Glint creates a separate T 1. Click the little gear icon of "TypeScript and JavaScript Language Features", and select "Disable (Workspace)". 1. Reload the workspace. Glint will now take over TS language services. -![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) \ No newline at end of file +![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) diff --git a/packages/core/src/cli/run-volar-tsc.ts b/packages/core/src/cli/run-volar-tsc.ts index 7dba75c59..02f3deb12 100644 --- a/packages/core/src/cli/run-volar-tsc.ts +++ b/packages/core/src/cli/run-volar-tsc.ts @@ -1,12 +1,35 @@ import { runTsc } from '@volar/typescript/lib/quickstart/runTsc.js'; import { createEmberLanguagePlugin } from '../volar/ember-language-plugin.js'; -import { findConfig } from '../config/index.js'; +import { findConfig, createTempConfigForFiles, findTypeScript } from '../config/index.js'; import { createRequire } from 'node:module'; +import { LanguagePlugin, URI } from '@volar/language-server'; const require = createRequire(import.meta.url); export function run(): void { - let cwd = process.cwd(); + const cwd = process.cwd(); + const args = process.argv.slice(2); + + // Use TypeScript's built-in command line parser + const ts = findTypeScript(cwd); + + if (!ts) { + throw new Error('TypeScript not found. Glint requires TypeScript to be installed.'); + } + + const parsedCommandLine = ts.parseCommandLine(args); + + // Handle parsing errors + if (parsedCommandLine.errors.length > 0) { + parsedCommandLine.errors.forEach(error => { + console.error(ts.flattenDiagnosticMessageText(error.messageText, '\n')); + }); + process.exit(1); + } + + const files = parsedCommandLine.fileNames; + const compilerOptions = parsedCommandLine.options; + const hasSpecificFiles = files.length > 0; const options = { extraSupportedExtensions: ['.gjs', '.gts'], @@ -21,16 +44,56 @@ export function run(): void { // See discussion here: https://github.com/typed-ember/glint/issues/628 }; - const main = (): void => - runTsc(require.resolve('typescript/lib/tsc'), options, (ts, options) => { - const glintConfig = findConfig(cwd); + const createLanguagePlugin = (): LanguagePlugin[] => { + const glintConfig = findConfig(cwd); + return glintConfig ? [createEmberLanguagePlugin(glintConfig)] : []; + }; - if (glintConfig) { - const gtsLanguagePlugin = createEmberLanguagePlugin(glintConfig); - return [gtsLanguagePlugin]; - } else { - return []; + if (hasSpecificFiles) { + // For specific files, create temporary tsconfig that inherits from project config + const { tempConfigPath, cleanup } = createTempConfigForFiles(cwd, files); + const originalArgv = process.argv; + + try { + // Build TypeScript arguments for single file checking + const tscArgs = ['node', 'tsc', '--project', tempConfigPath]; + + // Convert compiler options back to command line arguments + // Skip conflicting options that we control + const filteredOptions = { ...compilerOptions }; + delete filteredOptions.project; + + // Add --noEmit as default only if user hasn't specified emit-related flags + const hasEmitFlag = Boolean( + compilerOptions.noEmit || + compilerOptions.declaration || + compilerOptions.emitDeclarationOnly || + compilerOptions['build'] + ); + + if (!hasEmitFlag) { + filteredOptions.noEmit = true; } - }); - main(); + + // Convert options back to command line format + Object.entries(filteredOptions).forEach(([key, value]) => { + if (value === true) { + tscArgs.push(`--${key}`); + } else if (value === false) { + // Skip false boolean values + } else if (value !== undefined) { + tscArgs.push(`--${key}`, String(value)); + } + }); + + process.argv = tscArgs; + + runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin); + } finally { + cleanup(); + process.argv = originalArgv; + } + } else { + runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin); + } } diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 5d461da34..5ebda71d8 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -4,7 +4,7 @@ import { ConfigLoader } from './loader.js'; export { GlintConfig } from './config.js'; export { GlintEnvironment } from './environment.js'; -export { ConfigLoader, findTypeScript } from './loader.js'; +export { ConfigLoader, findTypeScript, createTempConfigForFiles } from './loader.js'; /** * Loads glint configuration, starting from the given directory diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index 798dcdcd8..d3d164643 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -139,3 +139,52 @@ function assert(test: unknown, message: string): asserts test { throw new SilentError(`Glint config: ${message}`); } } + +interface TempConfigResult { + tempConfigPath: string; + cleanup: () => void; +} + +/** + * Creates a temporary tsconfig.json for specific files while preserving project configuration. + */ +export function createTempConfigForFiles(cwd: string, fileArgs: string[]): TempConfigResult { + const ts = findTypeScript(cwd); + if (!ts) { + throw new Error('TypeScript not found. Glint requires TypeScript to be installed.'); + } + + const tsconfigPath = findNearestConfigFile(ts, cwd); + if (!tsconfigPath) { + throw new Error('No tsconfig.json found. Glint requires a TypeScript configuration file.'); + } + + const originalConfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8')); + const tempConfig = { + ...originalConfig, + files: fileArgs, + include: undefined, + exclude: undefined + }; + + const tempConfigPath = path.join(cwd, 'tsconfig.glint-temp.json'); + + fs.writeFileSync(tempConfigPath, JSON.stringify(tempConfig, null, 2)); + + const cleanup = (): void => { + try { + if (fs.existsSync(tempConfigPath)) { + fs.unlinkSync(tempConfigPath); + } + } catch { + // Ignore cleanup errors + } + }; + + // Setup cleanup on process exit + process.on('exit', cleanup); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + + return { tempConfigPath, cleanup }; +} diff --git a/test-packages/package-test-core/__tests__/cli/single-file.test.ts b/test-packages/package-test-core/__tests__/cli/single-file.test.ts new file mode 100644 index 000000000..4d56b263c --- /dev/null +++ b/test-packages/package-test-core/__tests__/cli/single-file.test.ts @@ -0,0 +1,105 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { describe, beforeEach, afterEach, test, expect } from 'vitest'; +import { createTempConfigForFiles } from '@glint/core/config/loader'; + +describe('CLI: single file checking', () => { + const testDir = `${os.tmpdir()}/glint-cli-test-${process.pid}`; + + beforeEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + fs.mkdirSync(testDir, { recursive: true }); + + // Create a minimal tsconfig.json + fs.writeFileSync( + path.join(testDir, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + target: 'ES2015', + module: 'commonjs', + strict: true + }, + glint: { + environment: 'ember-loose' + } + }, null, 2) + ); + + // Create a test file + fs.writeFileSync( + path.join(testDir, 'test.gts'), + `import Component from '@glimmer/component'; + +export default class Test extends Component { + +}` + ); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + test('creates temp config for single file', () => { + const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, ['test.gts']); + + try { + // Check temp config exists + expect(fs.existsSync(tempConfigPath)).toBe(true); + + // Check temp config content + const tempConfig = JSON.parse(fs.readFileSync(tempConfigPath, 'utf-8')); + expect(tempConfig.files).toEqual(['test.gts']); + expect(tempConfig.include).toBeUndefined(); + expect(tempConfig.exclude).toBeUndefined(); + expect(tempConfig.compilerOptions.target).toBe('ES2015'); + expect(tempConfig.glint.environment).toBe('ember-loose'); + } finally { + cleanup(); + } + + // Check cleanup worked + expect(fs.existsSync(tempConfigPath)).toBe(false); + }); + + test('creates temp config for multiple files', () => { + // Create another test file + fs.writeFileSync( + path.join(testDir, 'test2.gts'), + `import Component from '@glimmer/component'; + +export default class Test2 extends Component { + +}` + ); + + const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, ['test.gts', 'test2.gts']); + + try { + const tempConfig = JSON.parse(fs.readFileSync(tempConfigPath, 'utf-8')); + expect(tempConfig.files).toEqual(['test.gts', 'test2.gts']); + } finally { + cleanup(); + } + }); + + test('handles missing tsconfig', () => { + fs.unlinkSync(path.join(testDir, 'tsconfig.json')); + + expect(() => { + createTempConfigForFiles(testDir, ['test.gts']); + }).toThrow('No tsconfig.json found'); + }); + + test('cleanup is fired', () => { + const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, ['test.gts']); + + // Cleanup once + cleanup(); + expect(fs.existsSync(tempConfigPath)).toBe(false); + + // Cleanup again - should not throw + expect(() => cleanup()).not.toThrow(); + }); +}); From 138598b61a52747263d3be3120e95f3f21959001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Wed, 20 Aug 2025 15:08:50 +0200 Subject: [PATCH 2/8] Remove content from old docs and introduce ts.readConfig file instead of JSON.parse --- docs/getting-started.md | 27 --------------------------- packages/core/src/config/loader.ts | 8 +++++++- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 71f70d103..dd58d9117 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -36,33 +36,6 @@ To this: You can also use the `glint` command locally with the `--watch` flag to monitor your project as you work! -#### Single File Checking - -Glint supports checking individual files or a specific set of files instead of your entire project. This can be useful for faster feedback during development or when working with large codebases. - -```bash -# Check a single file -npx glint src/components/my-component.gts - -# Check multiple files -npx glint src/components/header.gts src/components/footer.gts - -# Check files with different extensions -npx glint src/helpers/format-date.ts src/components/date-picker.gts -``` - -When checking specific files, Glint: -- Uses your project's `tsconfig.json` configuration -- Applies the same type checking rules as project-wide checking -- Only analyzes the specified files for faster performance -- Maintains all your project's compiler options and path mappings - -This is particularly useful for: -- **IDE integrations** that need to check files on save -- **Git hooks** that validate only changed files -- **Development workflows** where you want quick feedback on specific components -- **CI optimizations** for incremental builds - ### Glint Editor Extensions You can install an editor extension to display Glint's diagnostics inline in your templates and provide richer editor support—typechecking, type information on hover, automated refactoring, and more—powered by `glint-language-server`: diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index d3d164643..4def6ee71 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -159,7 +159,13 @@ export function createTempConfigForFiles(cwd: string, fileArgs: string[]): TempC throw new Error('No tsconfig.json found. Glint requires a TypeScript configuration file.'); } - const originalConfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8')); + // Use TypeScript's config file reader to handle comments + const configFileResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + if (configFileResult.error) { + throw new Error(`Error reading tsconfig: ${ts.flattenDiagnosticMessageText(configFileResult.error.messageText, '\n')}`); + } + + const originalConfig = configFileResult.config; const tempConfig = { ...originalConfig, files: fileArgs, From feedfd26e42b1a3241047dcf554f52700fa6fb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Wed, 20 Aug 2025 15:27:26 +0200 Subject: [PATCH 3/8] Fix docs --- docs-v2/getting-started.md | 8 -------- docs/getting-started.md | 2 -- 2 files changed, 10 deletions(-) diff --git a/docs-v2/getting-started.md b/docs-v2/getting-started.md index dd9b17da4..2c704f254 100644 --- a/docs-v2/getting-started.md +++ b/docs-v2/getting-started.md @@ -72,12 +72,6 @@ When checking specific files, Glint: - Only analyzes the specified files for faster performance - Maintains all your project's compiler options and path mappings -This is particularly useful for: -- **IDE integrations** that need to check files on save -- **Git hooks** that validate only changed files -- **Development workflows** where you want quick feedback on specific components -- **CI optimizations** for incremental builds - ### Glint Editor Extensions You can install an editor extension to display Glint's diagnostics inline in your templates and provide richer editor support—typechecking, type information on hover, automated refactoring, and more—powered by `glint-language-server`: @@ -92,5 +86,3 @@ To get Ember/Glimmer and TypeScript working together, Glint creates a separate T 1. Type `@builtin typescript` in the extension search box 1. Click the little gear icon of "TypeScript and JavaScript Language Features", and select "Disable (Workspace)". 1. Reload the workspace. Glint will now take over TS language services. - -![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) diff --git a/docs/getting-started.md b/docs/getting-started.md index dd58d9117..64fb8f6a9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -50,5 +50,3 @@ To get Ember/Glimmer and TypeScript working together, Glint creates a separate T 1. Type `@builtin typescript` in the extension search box 1. Click the little gear icon of "TypeScript and JavaScript Language Features", and select "Disable (Workspace)". 1. Reload the workspace. Glint will now take over TS language services. - -![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png) From b569534a53d7462397095c3b945086114fe1a6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Wed, 20 Aug 2025 15:29:26 +0200 Subject: [PATCH 4/8] Removed unused function --- packages/core/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 5ebda71d8..5d461da34 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -4,7 +4,7 @@ import { ConfigLoader } from './loader.js'; export { GlintConfig } from './config.js'; export { GlintEnvironment } from './environment.js'; -export { ConfigLoader, findTypeScript, createTempConfigForFiles } from './loader.js'; +export { ConfigLoader, findTypeScript } from './loader.js'; /** * Loads glint configuration, starting from the given directory From d9724e7b858317de98ae498ef860976c543a2082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Wed, 20 Aug 2025 15:45:24 +0200 Subject: [PATCH 5/8] Fix docs --- docs-v2/getting-started.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs-v2/getting-started.md b/docs-v2/getting-started.md index 2c704f254..967364165 100644 --- a/docs-v2/getting-started.md +++ b/docs-v2/getting-started.md @@ -69,8 +69,7 @@ npx glint src/helpers/format-date.ts src/components/date-picker.gts When checking specific files, Glint: - Uses your project's `tsconfig.json` configuration - Applies the same type checking rules as project-wide checking -- Only analyzes the specified files for faster performance -- Maintains all your project's compiler options and path mappings +- Analyzes the specified files and all their dependencies for faster performance ### Glint Editor Extensions From b05380ec173ee037a7ac3da50e190be2435c9780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Thu, 21 Aug 2025 12:24:21 +0200 Subject: [PATCH 6/8] Fix tests --- packages/core/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 5d461da34..5ebda71d8 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -4,7 +4,7 @@ import { ConfigLoader } from './loader.js'; export { GlintConfig } from './config.js'; export { GlintEnvironment } from './environment.js'; -export { ConfigLoader, findTypeScript } from './loader.js'; +export { ConfigLoader, findTypeScript, createTempConfigForFiles } from './loader.js'; /** * Loads glint configuration, starting from the given directory From 87def4081ad5bd1b2ae394ecb24dee7bb3d2ee25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Thu, 21 Aug 2025 12:40:11 +0200 Subject: [PATCH 7/8] Encapsulate the process.argv mutation --- packages/core/src/cli/run-volar-tsc.ts | 22 +++++++++------------- packages/core/src/cli/utils.ts | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/cli/utils.ts diff --git a/packages/core/src/cli/run-volar-tsc.ts b/packages/core/src/cli/run-volar-tsc.ts index 02f3deb12..4117812ec 100644 --- a/packages/core/src/cli/run-volar-tsc.ts +++ b/packages/core/src/cli/run-volar-tsc.ts @@ -4,6 +4,8 @@ import { findConfig, createTempConfigForFiles, findTypeScript } from '../config/ import { createRequire } from 'node:module'; import { LanguagePlugin, URI } from '@volar/language-server'; +import { runTscWithArgs } from './utils.js'; + const require = createRequire(import.meta.url); export function run(): void { @@ -52,7 +54,6 @@ export function run(): void { if (hasSpecificFiles) { // For specific files, create temporary tsconfig that inherits from project config const { tempConfigPath, cleanup } = createTempConfigForFiles(cwd, files); - const originalArgv = process.argv; try { // Build TypeScript arguments for single file checking @@ -76,22 +77,17 @@ export function run(): void { } // Convert options back to command line format - Object.entries(filteredOptions).forEach(([key, value]) => { - if (value === true) { - tscArgs.push(`--${key}`); - } else if (value === false) { - // Skip false boolean values - } else if (value !== undefined) { - tscArgs.push(`--${key}`, String(value)); - } - }); + const compilerArgs = Object.entries(filteredOptions) + .filter(([, value]) => value !== false && value !== undefined) + .flatMap(([key, value]) => + value === true ? [`--${key}`] : [`--${key}`, String(value)] + ); - process.argv = tscArgs; + tscArgs.push(...compilerArgs); - runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin); + runTscWithArgs(require.resolve('typescript/lib/tsc'), tscArgs, options, createLanguagePlugin); } finally { cleanup(); - process.argv = originalArgv; } } else { runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin); diff --git a/packages/core/src/cli/utils.ts b/packages/core/src/cli/utils.ts new file mode 100644 index 000000000..5ff53e16f --- /dev/null +++ b/packages/core/src/cli/utils.ts @@ -0,0 +1,22 @@ +import { runTsc } from '@volar/typescript/lib/quickstart/runTsc.js'; +import type { LanguagePlugin, URI } from '@volar/language-server'; + +/** + * Helper function to run tsc with custom arguments while safely managing process.argv. + * This encapsulates the process.argv mutation to avoid polluting global state. + */ +export function runTscWithArgs( + tscPath: string, + args: string[], + options: any, + createLanguagePlugin: () => LanguagePlugin[] +): void { + const originalArgv = process.argv; + try { + process.argv = args; + runTsc(tscPath, options, createLanguagePlugin); + } finally { + // Always restore original argv, even if runTsc throws + process.argv = originalArgv; + } +} From 711e6531c5b5d6c642817c6902caa5e793c00f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Ratkovi=C4=87?= Date: Sat, 23 Aug 2025 16:40:47 +0200 Subject: [PATCH 8/8] Run prettier --write --- packages/core/src/cli/run-volar-tsc.ts | 34 ++++++++------- packages/core/src/cli/utils.ts | 2 +- packages/core/src/config/loader.ts | 8 ++-- .../__tests__/cli/single-file.test.ts | 41 +++++++++++-------- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/packages/core/src/cli/run-volar-tsc.ts b/packages/core/src/cli/run-volar-tsc.ts index 4117812ec..d8f8cf1b0 100644 --- a/packages/core/src/cli/run-volar-tsc.ts +++ b/packages/core/src/cli/run-volar-tsc.ts @@ -11,24 +11,24 @@ const require = createRequire(import.meta.url); export function run(): void { const cwd = process.cwd(); const args = process.argv.slice(2); - + // Use TypeScript's built-in command line parser const ts = findTypeScript(cwd); if (!ts) { throw new Error('TypeScript not found. Glint requires TypeScript to be installed.'); } - + const parsedCommandLine = ts.parseCommandLine(args); - + // Handle parsing errors if (parsedCommandLine.errors.length > 0) { - parsedCommandLine.errors.forEach(error => { + parsedCommandLine.errors.forEach((error) => { console.error(ts.flattenDiagnosticMessageText(error.messageText, '\n')); }); process.exit(1); } - + const files = parsedCommandLine.fileNames; const compilerOptions = parsedCommandLine.options; const hasSpecificFiles = files.length > 0; @@ -54,37 +54,35 @@ export function run(): void { if (hasSpecificFiles) { // For specific files, create temporary tsconfig that inherits from project config const { tempConfigPath, cleanup } = createTempConfigForFiles(cwd, files); - + try { // Build TypeScript arguments for single file checking const tscArgs = ['node', 'tsc', '--project', tempConfigPath]; - + // Convert compiler options back to command line arguments // Skip conflicting options that we control const filteredOptions = { ...compilerOptions }; delete filteredOptions.project; - + // Add --noEmit as default only if user hasn't specified emit-related flags const hasEmitFlag = Boolean( compilerOptions.noEmit || - compilerOptions.declaration || - compilerOptions.emitDeclarationOnly || - compilerOptions['build'] + compilerOptions.declaration || + compilerOptions.emitDeclarationOnly || + compilerOptions['build'], ); - + if (!hasEmitFlag) { filteredOptions.noEmit = true; } - + // Convert options back to command line format const compilerArgs = Object.entries(filteredOptions) .filter(([, value]) => value !== false && value !== undefined) - .flatMap(([key, value]) => - value === true ? [`--${key}`] : [`--${key}`, String(value)] - ); - + .flatMap(([key, value]) => (value === true ? [`--${key}`] : [`--${key}`, String(value)])); + tscArgs.push(...compilerArgs); - + runTscWithArgs(require.resolve('typescript/lib/tsc'), tscArgs, options, createLanguagePlugin); } finally { cleanup(); diff --git a/packages/core/src/cli/utils.ts b/packages/core/src/cli/utils.ts index 5ff53e16f..42cfb3fb8 100644 --- a/packages/core/src/cli/utils.ts +++ b/packages/core/src/cli/utils.ts @@ -9,7 +9,7 @@ export function runTscWithArgs( tscPath: string, args: string[], options: any, - createLanguagePlugin: () => LanguagePlugin[] + createLanguagePlugin: () => LanguagePlugin[], ): void { const originalArgv = process.argv; try { diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index 4def6ee71..c8b5da3cd 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -162,15 +162,17 @@ export function createTempConfigForFiles(cwd: string, fileArgs: string[]): TempC // Use TypeScript's config file reader to handle comments const configFileResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile); if (configFileResult.error) { - throw new Error(`Error reading tsconfig: ${ts.flattenDiagnosticMessageText(configFileResult.error.messageText, '\n')}`); + throw new Error( + `Error reading tsconfig: ${ts.flattenDiagnosticMessageText(configFileResult.error.messageText, '\n')}`, + ); } - + const originalConfig = configFileResult.config; const tempConfig = { ...originalConfig, files: fileArgs, include: undefined, - exclude: undefined + exclude: undefined, }; const tempConfigPath = path.join(cwd, 'tsconfig.glint-temp.json'); diff --git a/test-packages/package-test-core/__tests__/cli/single-file.test.ts b/test-packages/package-test-core/__tests__/cli/single-file.test.ts index 4d56b263c..daa40ff67 100644 --- a/test-packages/package-test-core/__tests__/cli/single-file.test.ts +++ b/test-packages/package-test-core/__tests__/cli/single-file.test.ts @@ -10,20 +10,24 @@ describe('CLI: single file checking', () => { beforeEach(() => { fs.rmSync(testDir, { recursive: true, force: true }); fs.mkdirSync(testDir, { recursive: true }); - + // Create a minimal tsconfig.json fs.writeFileSync( path.join(testDir, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - target: 'ES2015', - module: 'commonjs', - strict: true + JSON.stringify( + { + compilerOptions: { + target: 'ES2015', + module: 'commonjs', + strict: true, + }, + glint: { + environment: 'ember-loose', + }, }, - glint: { - environment: 'ember-loose' - } - }, null, 2) + null, + 2, + ), ); // Create a test file @@ -33,7 +37,7 @@ describe('CLI: single file checking', () => { export default class Test extends Component { -}` +}`, ); }); @@ -47,7 +51,7 @@ export default class Test extends Component { try { // Check temp config exists expect(fs.existsSync(tempConfigPath)).toBe(true); - + // Check temp config content const tempConfig = JSON.parse(fs.readFileSync(tempConfigPath, 'utf-8')); expect(tempConfig.files).toEqual(['test.gts']); @@ -71,10 +75,13 @@ export default class Test extends Component { export default class Test2 extends Component { -}` +}`, ); - const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, ['test.gts', 'test2.gts']); + const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, [ + 'test.gts', + 'test2.gts', + ]); try { const tempConfig = JSON.parse(fs.readFileSync(tempConfigPath, 'utf-8')); @@ -86,7 +93,7 @@ export default class Test2 extends Component { test('handles missing tsconfig', () => { fs.unlinkSync(path.join(testDir, 'tsconfig.json')); - + expect(() => { createTempConfigForFiles(testDir, ['test.gts']); }).toThrow('No tsconfig.json found'); @@ -94,11 +101,11 @@ export default class Test2 extends Component { test('cleanup is fired', () => { const { tempConfigPath, cleanup } = createTempConfigForFiles(testDir, ['test.gts']); - + // Cleanup once cleanup(); expect(fs.existsSync(tempConfigPath)).toBe(false); - + // Cleanup again - should not throw expect(() => cleanup()).not.toThrow(); });