diff --git a/docs-v2/getting-started.md b/docs-v2/getting-started.md index 2d513eb64..967364165 100644 --- a/docs-v2/getting-started.md +++ b/docs-v2/getting-started.md @@ -51,6 +51,26 @@ 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 +- Analyzes the specified files and all their dependencies for faster performance + ### 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`: @@ -65,5 +85,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) \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md index 58d39438a..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) \ No newline at end of file diff --git a/packages/core/src/cli/run-volar-tsc.ts b/packages/core/src/cli/run-volar-tsc.ts index 7dba75c59..d8f8cf1b0 100644 --- a/packages/core/src/cli/run-volar-tsc.ts +++ b/packages/core/src/cli/run-volar-tsc.ts @@ -1,12 +1,37 @@ 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'; +import { runTscWithArgs } from './utils.js'; + 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 +46,48 @@ 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 (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'], + ); - if (glintConfig) { - const gtsLanguagePlugin = createEmberLanguagePlugin(glintConfig); - return [gtsLanguagePlugin]; - } else { - return []; + if (!hasEmitFlag) { + filteredOptions.noEmit = true; } - }); - main(); + + // 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)])); + + tscArgs.push(...compilerArgs); + + runTscWithArgs(require.resolve('typescript/lib/tsc'), tscArgs, options, createLanguagePlugin); + } finally { + cleanup(); + } + } 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..42cfb3fb8 --- /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; + } +} 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..c8b5da3cd 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -139,3 +139,60 @@ 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.'); + } + + // 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, + 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..daa40ff67 --- /dev/null +++ b/test-packages/package-test-core/__tests__/cli/single-file.test.ts @@ -0,0 +1,112 @@ +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(); + }); +});