Skip to content

Commit 0b82ced

Browse files
committed
Support standalone templates in @glint/cli
1 parent 3d5177d commit 0b82ced

File tree

7 files changed

+142
-13
lines changed

7 files changed

+142
-13
lines changed

packages/cli/__tests__/check.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('single-pass typechecking', () => {
7575
`);
7676
});
7777

78-
test('reports diagnostics for a template type error', async () => {
78+
test('reports diagnostics for an inline template type error', async () => {
7979
let code = stripIndent`
8080
import Component, { hbs } from '@glimmerx/component';
8181
@@ -113,6 +113,46 @@ describe('single-pass typechecking', () => {
113113
`);
114114
});
115115

116+
test('reports diagnostics for a companion template type error', async () => {
117+
project.write('.glintrc', 'environment: ember-loose\n');
118+
119+
let script = stripIndent`
120+
import Component from '@ember/component';
121+
122+
export interface MyComponentArgs {
123+
message: string;
124+
}
125+
126+
export default class MyComponent extends Component<MyComponentArgs> {
127+
target = 'World!';
128+
}
129+
`;
130+
131+
let template = stripIndent`
132+
{{@message}}, {{this.targett}}
133+
`;
134+
135+
project.write('my-component.ts', script);
136+
project.write('my-component.hbs', template);
137+
138+
let checkResult = await project.check({ reject: false });
139+
140+
expect(checkResult.exitCode).toBe(1);
141+
expect(checkResult.stdout).toEqual('');
142+
expect(stripAnsi(checkResult.stderr)).toMatchInlineSnapshot(`
143+
"my-component.hbs:1:22 - error TS2551: Property 'targett' does not exist on type 'MyComponent'. Did you mean 'target'?
144+
145+
1 {{@message}}, {{this.targett}}
146+
~~~~~~~
147+
148+
my-component.ts:8:3
149+
8 target = 'World!';
150+
~~~~~~
151+
'target' is declared here.
152+
"
153+
`);
154+
});
155+
116156
test('honors .glintrc configuration', async () => {
117157
let code = stripIndent`
118158
import Component, { hbs } from '@glimmerx/component';

packages/cli/__tests__/utils/project.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export default class Project {
4949
return fs.readFileSync(this.filePath(fileName), 'utf-8');
5050
}
5151

52+
public remove(fileName: string): void {
53+
fs.unlinkSync(this.filePath(fileName));
54+
}
55+
5256
public async destroy(): Promise<void> {
5357
fs.rmdirSync(this.rootDir, { recursive: true });
5458
}

packages/cli/__tests__/watch.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,54 @@ describe('watched typechecking', () => {
166166

167167
await watch.terminate();
168168
});
169+
170+
test('reports on errors introduced and cleared in a companion template', async () => {
171+
project.write('.glintrc', 'environment: ember-loose\n');
172+
project.write('index.ts', 'import "@glint/environment-ember-loose/types";');
173+
174+
let script = stripIndent`
175+
import Component from '@ember/component';
176+
177+
export interface MyComponentArgs {
178+
message: string;
179+
}
180+
181+
export default class MyComponent extends Component<MyComponentArgs> {
182+
target = 'World!';
183+
}
184+
`;
185+
186+
let template = stripIndent`
187+
{{@message}}, {{this.target}}
188+
`;
189+
190+
project.write('my-component.ts', script);
191+
192+
let watch = project.watch({ reject: true });
193+
194+
let output = await watch.awaitOutput('Watching for file changes.');
195+
expect(output).toMatch('Found 0 errors.');
196+
197+
project.write('my-component.hbs', template.replace('target', 'tarrget'));
198+
199+
output = await watch.awaitOutput('Watching for file changes.');
200+
expect(output).toMatch('Found 1 error.');
201+
202+
project.write('my-component.hbs', template);
203+
204+
output = await watch.awaitOutput('Watching for file changes.');
205+
expect(output).toMatch('Found 0 errors.');
206+
207+
project.write('my-component.hbs', template.replace('@message', '@messagee'));
208+
209+
output = await watch.awaitOutput('Watching for file changes.');
210+
expect(output).toMatch('Found 1 error.');
211+
212+
project.remove('my-component.hbs');
213+
214+
output = await watch.awaitOutput('Watching for file changes.');
215+
expect(output).toMatch('Found 0 errors.');
216+
217+
await watch.terminate();
218+
});
169219
});

packages/cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"dependencies": {
2323
"@glint/config": "^0.2.1",
2424
"@glint/transform": "^0.2.1",
25-
"debug": "^4.1.1",
2625
"resolve": "^1.17.0",
2726
"yargs": "^15.3.1"
2827
},

packages/cli/src/perform-check.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ function createCompilerHost(
4646
transformManager: TransformManager
4747
): ts.CompilerHost {
4848
let host = ts.createCompilerHost(options);
49-
host.readFile = function (filename) {
50-
return transformManager.readFile(filename);
51-
};
49+
host.readFile = transformManager.readFile;
5250
return host;
5351
}
5452

packages/cli/src/perform-watch.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ function sysForWatchCompilerHost(
2727
): typeof ts.sys {
2828
return {
2929
...ts.sys,
30-
readFile(path, encoding) {
31-
return transformManager.readFile(path, encoding);
32-
},
30+
watchFile: transformManager.watchFile,
31+
readFile: transformManager.readFile,
3332
};
3433
}
3534

packages/cli/src/transform-manager.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TransformedModule, rewriteModule, rewriteDiagnostic } from '@glint/transform';
22
import type ts from 'typescript';
33
import { GlintConfig } from '@glint/config';
4+
import { assert } from '@glint/transform/lib/util';
45

56
export default class TransformManager {
67
private transformedModules = new Map<string, TransformedModule>();
@@ -29,27 +30,65 @@ export default class TransformManager {
2930
);
3031
}
3132

32-
public readFile(filename: string, encoding?: string): string | undefined {
33+
public watchFile = (
34+
path: string,
35+
callback: ts.FileWatcherCallback,
36+
pollingInterval?: number,
37+
options?: ts.WatchOptions
38+
): ts.FileWatcher => {
39+
const { watchFile } = this.ts.sys;
40+
assert(watchFile);
41+
42+
let rootWatcher = watchFile(path, callback, pollingInterval, options);
43+
let templatePaths = this.glintConfig.environment.getPossibleTemplatePaths(path);
44+
45+
if (this.glintConfig.includesFile(path) && templatePaths.length) {
46+
let templateWatchers = templatePaths.map((candidate) =>
47+
watchFile(candidate, (_, event) => callback(path, event), pollingInterval, options)
48+
);
49+
50+
return {
51+
close() {
52+
rootWatcher.close();
53+
templateWatchers.forEach((watcher) => watcher.close());
54+
},
55+
};
56+
}
57+
58+
return rootWatcher;
59+
};
60+
61+
public readFile = (filename: string, encoding?: string): string | undefined => {
3362
let contents = this.ts.sys.readFile(filename, encoding);
3463
let config = this.glintConfig;
3564

3665
if (
3766
contents &&
3867
filename.endsWith('.ts') &&
3968
!filename.endsWith('.d.ts') &&
40-
config.includesFile(filename) &&
41-
config.environment.moduleMayHaveTagImports(contents)
69+
config.includesFile(filename)
4270
) {
71+
let mayHaveTaggedTemplates = contents && config.environment.moduleMayHaveTagImports(contents);
72+
let templateCandidates = config.environment.getPossibleTemplatePaths(filename);
73+
let templatePath = templateCandidates.find((candidate) => this.ts.sys.fileExists(candidate));
74+
if (!mayHaveTaggedTemplates && !templatePath) {
75+
return contents;
76+
}
77+
4378
let script = { filename, contents };
44-
let transformedModule = rewriteModule({ script }, config.environment);
79+
let template = templatePath
80+
? { filename: templatePath, contents: this.ts.sys.readFile(templatePath) ?? '' }
81+
: undefined;
82+
83+
let transformedModule = rewriteModule({ script, template }, config.environment);
4584
if (transformedModule) {
4685
this.transformedModules.set(filename, transformedModule);
4786
return transformedModule.transformedContents;
4887
}
4988
}
5089

5190
return contents;
52-
}
91+
};
5392

5493
private readonly formatDiagnosticHost: ts.FormatDiagnosticsHost = {
5594
getCanonicalFileName: (name) => name,

0 commit comments

Comments
 (0)