Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 20 additions & 2 deletions docs-v2/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
- 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`:
Expand All @@ -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)
2 changes: 0 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
81 changes: 69 additions & 12 deletions packages/core/src/cli/run-volar-tsc.ts
Original file line number Diff line number Diff line change
@@ -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'],
Expand All @@ -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<URI>[] => {
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 ||
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 (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);
}
}
22 changes: 22 additions & 0 deletions packages/core/src/cli/utils.ts
Original file line number Diff line number Diff line change
@@ -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<URI>[],
): 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;
}
}
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
57 changes: 57 additions & 0 deletions packages/core/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
112 changes: 112 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,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 {
<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();
});
});
Loading