Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs-v2/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have numbers on the size of your project and how much of a difference this made?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, > 5000 files.

Running the glint command on all files can take up to 60 seconds (when used with lint-staged). Since we're experiencing issues with the Glint extension frequently crashing, broken code sometimes gets pushed upstream, and TypeScript errors are only caught later when the full command is executed in CI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint staged has a glint plugin? Do you have to use that?

Also, have you tried the v2 alphas and prererease in vscode?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint staged has a glint plugin? Do you have to use that?

No need for specific plugins, you can use it like this.

{
  "lint-staged": {
    "*.{gts,ts}": [
      "glint --noEmit"
    ]
  }
}

I wouldn't say we have to use it that way, but it would be super convenient if, along with other linters, it could run glint (lint) on the changed files at commit time together with stylelint, for example, and the others...

Also, have you tried the v2 alphas and prererease in vscode?

I don't think so...

Copy link
Contributor

@NullVoxPopuli NullVoxPopuli Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could run glint (lint) on the changed files at commit time together with stylelint, for example, and the others...

For sure, but typescript doesn't work this way, it has to resolve the types of everything imported.

But! I will concede if with this branch, the outputs are significantly different:

# for reference 
glint --version
glint -v
# measure
time glint
time glint ./app/templates/application.gts
time glint ./app/components/something.gts

If you could get me these numbers, that'd be a huge help

I don't think so...

You'll want to, as we've frozen glint v1 and are only working on glint v2 right now. It's already way faster.
(And this pr is targeting v2)

Caveat tho, we dropped support for hbs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node glint/packages/core/bin/glint.js --version: Version 5.8.2

time node glint/packages/core/bin/glint.js: 19.93s user 2.93s system 173% cpu 13.200 total
time node glint/packages/core/bin/glint.js app/components/feature-badge.gts: 1.58s user 0.19s system 189% cpu 0.934 total
time node glint/packages/core/bin/glint.js app/components/scrollable-page.gts app/components/feature-badge.gts : 1.50s user 0.17s system 188% cpu 0.881 total

It correctly detects the errors I intentionally introduce, so everything seems to be working fine.

Also, the 19 seconds is because I ran Glint in just one project (we have a monorepo with multiple projects and packages), so it would take even longer otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty good results!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, a file with multiple imports will resolve and lint all those imported files as well. But even so, it’s still significantly faster than always running the Glint command across the entire codebase.


```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`:
Expand All @@ -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)
![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png)
29 changes: 28 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand All @@ -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)
![Disabling built-in TS language service per workspace](https://user-images.githubusercontent.com/108688/111069039-6dc84100-84cb-11eb-8339-18a589be2ac5.png)
87 changes: 75 additions & 12 deletions packages/core/src/cli/run-volar-tsc.ts
Original file line number Diff line number Diff line change
@@ -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'],
Expand All @@ -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<URI>[] => {
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 ||
Comment on lines +68 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should support emitting at all in this mode.

You'd get a partial project, which, if you're a library, would be bad for your consumers.

Applications don't have any reason to emit at all, iirc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to support emit, as long as the default is noEmit, so why not let users decide? Maybe someone just wants to test a single file and see the final JS output for whatever reason.

But whatever you say, I'm more or less fine with either way.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Argh should be treated as read only.

Mutating argv can break other code that reads argv

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted the process.argv mutation into a helper function so we don't pollute global state and the cleanup is guaranteed to happen even if TypeScript throws an error.


runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin);
} finally {
cleanup();
process.argv = originalArgv;
}
} else {
runTsc(require.resolve('typescript/lib/tsc'), options, createLanguagePlugin);
}
}
2 changes: 1 addition & 1 deletion packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions packages/core/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
105 changes: 105 additions & 0 deletions test-packages/package-test-core/__tests__/cli/single-file.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
<template>Hello World!</template>
}`
);
});

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 {
<template>Hello Second!</template>
}`
);

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();
});
});