Skip to content

Commit 669af32

Browse files
committed
Fix project argument to match tsc
The `--project` argument was being treated as a path to some location inside the project, rather than as either a path to a tsconfig file or a path to a folder containing a tsconfig file, which is how `tsc` implements it. This meant that there was no way to run `glint` and use a config file named anything other than `tsconfig.json`. This is a breaking change because if anybody was passing a project path that pointed deeply into the project, that will no longer work. Similarly, the `analyzeProject()` unstable API's behavior has changed, and `loadConfig` export has been replaced with `loadClosestConfig` and `loadConfigFromProject` exports.
1 parent 8d9e274 commit 669af32

File tree

6 files changed

+231
-44
lines changed

6 files changed

+231
-44
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as fs from 'node:fs';
22
import * as os from 'node:os';
33
import { describe, beforeEach, afterEach, test, expect } from 'vitest';
4-
import { loadConfig } from '../../src/config/index.js';
4+
import { loadClosestConfig, loadConfigFromProject } from '../../src/config/index.js';
55
import { normalizePath } from '../../src/config/config.js';
66

7-
describe('Config: loadConfig', () => {
7+
describe('Config', () => {
88
const testDir = `${os.tmpdir()}/glint-config-test-load-config-${process.pid}`;
99

1010
beforeEach(() => {
@@ -20,35 +20,111 @@ describe('Config: loadConfig', () => {
2020
fs.rmSync(testDir, { recursive: true, force: true });
2121
});
2222

23-
test('throws an error if no config is found', () => {
24-
expect(() => loadConfig(testDir)).toThrow(`Unable to find Glint configuration for ${testDir}`);
23+
describe('loadClosestConfig', () => {
24+
test('throws an error if no config is found', () => {
25+
expect(() => loadClosestConfig(testDir)).toThrow(
26+
`Unable to find Glint configuration for ${testDir}`
27+
);
28+
});
29+
30+
test('loads from a folder', () => {
31+
fs.writeFileSync(
32+
`${testDir}/tsconfig.json`,
33+
JSON.stringify({
34+
glint: {
35+
environment: './local-env',
36+
},
37+
})
38+
);
39+
40+
let config = loadClosestConfig(`${testDir}/deeply/nested/directory`);
41+
42+
expect(config.rootDir).toBe(normalizePath(testDir));
43+
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
44+
});
45+
46+
test('locates config in a parent directory', () => {
47+
fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true });
48+
fs.writeFileSync(
49+
`${testDir}/tsconfig.json`,
50+
JSON.stringify({
51+
glint: {
52+
environment: 'kaboom',
53+
checkStandaloneTemplates: false,
54+
},
55+
})
56+
);
57+
fs.writeFileSync(
58+
`${testDir}/deeply/tsconfig.json`,
59+
JSON.stringify({
60+
extends: '../tsconfig.json',
61+
glint: {
62+
environment: '../local-env',
63+
},
64+
})
65+
);
66+
67+
let config = loadClosestConfig(`${testDir}/deeply/nested/directory`);
68+
69+
expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`));
70+
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
71+
expect(config.checkStandaloneTemplates).toBe(false);
72+
});
2573
});
2674

27-
test('locates config in a parent directory', () => {
28-
fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true });
29-
fs.writeFileSync(
30-
`${testDir}/tsconfig.json`,
31-
JSON.stringify({
32-
glint: {
33-
environment: 'kaboom',
34-
checkStandaloneTemplates: false,
35-
},
36-
})
37-
);
38-
fs.writeFileSync(
39-
`${testDir}/deeply/tsconfig.json`,
40-
JSON.stringify({
41-
extends: '../tsconfig.json',
42-
glint: {
43-
environment: '../local-env',
44-
},
45-
})
46-
);
75+
describe('loadConfigFromProject', () => {
76+
test('throws an error if no config is found', () => {
77+
expect(() => loadConfigFromProject(testDir)).toThrow(
78+
`Unable to find Glint configuration for project ${testDir}`
79+
);
80+
expect(() => loadConfigFromProject(`${testDir}/tsconfig.json`)).toThrow(
81+
`Unable to find Glint configuration for project ${testDir}`
82+
);
83+
});
84+
85+
test('loads from a folder', () => {
86+
fs.writeFileSync(
87+
`${testDir}/tsconfig.json`,
88+
JSON.stringify({
89+
glint: {
90+
environment: './local-env',
91+
},
92+
})
93+
);
94+
95+
expect(loadConfigFromProject(testDir).rootDir).toBe(normalizePath(testDir));
96+
});
97+
98+
test('loads from a file', () => {
99+
fs.writeFileSync(
100+
`${testDir}/tsconfig.custom.json`,
101+
JSON.stringify({
102+
glint: {
103+
environment: './local-env',
104+
},
105+
})
106+
);
107+
108+
expect(loadConfigFromProject(`${testDir}/tsconfig.custom.json`).rootDir).toBe(
109+
normalizePath(testDir)
110+
);
111+
});
47112

48-
let config = loadConfig(`${testDir}/deeply/nested/directory`);
113+
test('does not search parent directories', () => {
114+
fs.mkdirSync(`${testDir}/sub`, { recursive: true });
115+
fs.writeFileSync(
116+
`${testDir}/tsconfig.json`,
117+
JSON.stringify({
118+
glint: {
119+
environment: 'kaboom',
120+
checkStandaloneTemplates: false,
121+
},
122+
})
123+
);
49124

50-
expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`));
51-
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
52-
expect(config.checkStandaloneTemplates).toBe(false);
125+
expect(() => loadConfigFromProject(`${testDir}/sub`)).toThrow(
126+
`Unable to find Glint configuration for project ${testDir}`
127+
);
128+
});
53129
});
54130
});

packages/core/__tests__/config/loader.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,54 @@ describe('Config: loadConfig', () => {
6262
let loader = new ConfigLoader();
6363
let configA = loader.configForFile(`${testDir}/src/a.ts`);
6464
let configB = loader.configForFile(`${testDir}/src/b.ts`);
65+
let configC = loader.configForFile(`${testDir}/src/../src/c.ts`);
6566

6667
expect(configA).toBe(configB);
68+
expect(configA).toBe(configC);
69+
});
70+
71+
test('returns config from project file path', () => {
72+
fs.writeFileSync(
73+
`${testDir}/tsconfig.customname.json`,
74+
JSON.stringify({
75+
glint: { environment: './local-env.js' },
76+
})
77+
);
78+
79+
expect(
80+
new ConfigLoader().configForProjectPath(`${testDir}/tsconfig.customname.json`)?.rootDir
81+
).toBe(normalizePath(`${testDir}`));
82+
});
83+
84+
test('returns config from project folder path', () => {
85+
fs.writeFileSync(
86+
`${testDir}/tsconfig.json`,
87+
JSON.stringify({
88+
glint: { environment: './local-env.js' },
89+
})
90+
);
91+
92+
expect(new ConfigLoader().configForProjectPath(testDir)?.rootDir).toBe(
93+
normalizePath(`${testDir}`)
94+
);
95+
});
96+
97+
test('returns null for invalid project paths', () => {
98+
fs.mkdirSync(`${testDir}/packages/a/src`, { recursive: true });
99+
100+
fs.writeFileSync(
101+
`${testDir}/tsconfig.json`,
102+
JSON.stringify({
103+
glint: { environment: './local-env.js' },
104+
})
105+
);
106+
107+
expect(
108+
new ConfigLoader().configForProjectPath(`${testDir}/tsconfig.missing.json`)
109+
).toBeNull();
110+
expect(
111+
new ConfigLoader().configForProjectPath(`${testDir}/packages/a/src`)
112+
).toBeNull();
67113
});
68114

69115
describe('extending other config', () => {

packages/core/src/cli/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createRequire } from 'node:module';
22
import yargs from 'yargs';
3-
import { findTypeScript, loadConfig } from '../config/index.js';
3+
import { findTypeScript, loadClosestConfig, loadConfigFromProject } from '../config/index.js';
44
import { performWatch } from './perform-watch.js';
55
import { performCheck } from './perform-check.js';
66
import { determineOptionsToExtend } from './options.js';
@@ -18,7 +18,7 @@ const argv = yargs(process.argv.slice(2))
1818
.option('project', {
1919
alias: 'p',
2020
string: true,
21-
description: 'The path to the tsconfig file to use',
21+
description: 'The path to the tsconfig file to use or the folder containing it',
2222
})
2323
.option('watch', {
2424
alias: 'w',
@@ -120,7 +120,8 @@ if (argv.build) {
120120
performBuild(ts, projects, buildOptions);
121121
}
122122
} else {
123-
const glintConfig = loadConfig(argv.project ?? cwd);
123+
const glintConfig =
124+
argv.project !== undefined ? loadConfigFromProject(argv.project) : loadClosestConfig(cwd);
124125
const optionsToExtend = determineOptionsToExtend(argv);
125126

126127
validateTSOrExit(glintConfig.ts);

packages/core/src/config/index.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,26 @@ export { GlintConfig } from './config.js';
66
export { GlintEnvironment } from './environment.js';
77
export { ConfigLoader, findTypeScript } from './loader.js';
88

9+
/**
10+
* Loads glint configuration from the specified project path. If a path to a
11+
* file is passed, the config is loaded from that file. If the path to a folder
12+
* is passed, the config is loaded from the `tsconfig.json` or `jsconfig.json`
13+
* file contained in that folder. Raises an error if no configuration is found.
14+
*/
15+
export function loadConfigFromProject(from: string): GlintConfig {
16+
let config = new ConfigLoader().configForProjectPath(from);
17+
if (!config) {
18+
throw new SilentError(`Unable to find Glint configuration for project ${from}`);
19+
}
20+
return config;
21+
}
22+
923
/**
1024
* Loads glint configuration, starting from the given directory
1125
* and searching upwards and raising an error if no configuration
1226
* is found.
1327
*/
14-
export function loadConfig(from: string): GlintConfig {
28+
export function loadClosestConfig(from: string): GlintConfig {
1529
let config = findConfig(from);
1630
if (!config) {
1731
throw new SilentError(`Unable to find Glint configuration for ${from}`);

packages/core/src/config/loader.ts

+57-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createRequire } from 'node:module';
22
import * as path from 'node:path';
3+
import * as fs from 'node:fs';
34
import SilentError from 'silent-error';
45
import { GlintConfig } from './config.js';
56
import { GlintConfigInput } from '@glint/core/config-types';
@@ -10,32 +11,72 @@ const require = createRequire(import.meta.url);
1011
type TypeScript = typeof TS;
1112

1213
/**
13-
* `ConfigLoader` provides an interface for finding the Glint config that
14-
* applies to a given file or directory, ensuring that only a single instance
15-
* of `GlintConfig` is ever created for a given `tsconfig.json` or
16-
* `jsconfig.json` source file.
14+
* `ConfigLoader` provides an interface for finding and loading GLint
15+
* configurations from config files (e.g. `tsconfig.json` or `jsconfig.json`),
16+
* and ensuring that only a single instance of `GlintConfig` is ever created for
17+
* a given config file.
1718
*/
1819
export class ConfigLoader {
1920
private configs = new Map<string, GlintConfig | null>();
2021

22+
/**
23+
* Given the path to a configuration file, or to a folder containing a
24+
* `tsconfig.json` or `jsconfig.json`, load the configuration. This is meant
25+
* to implement the behavior of `glint`/`tsc`'s `--project` command-line
26+
* option.
27+
*/
28+
public configForProjectPath(configPath: string): GlintConfig | null {
29+
let tsConfigPath = path.join(configPath, 'tsconfig.json');
30+
let jsConfigPath = path.join(configPath, 'tsconfig.json');
31+
32+
if (fileExists(configPath)) {
33+
return this.configForConfigFile(configPath);
34+
} else if (fileExists(tsConfigPath)) {
35+
return this.configForConfigFile(tsConfigPath);
36+
} else if (fileExists(jsConfigPath)) {
37+
return this.configForConfigFile(jsConfigPath);
38+
} else {
39+
return null;
40+
}
41+
}
42+
43+
/**
44+
* Given the path to a file, find the closest `tsconfig.json` or
45+
* `jsconfig.json` file in the directory structure and load its configuration.
46+
*/
2147
public configForFile(filePath: string): GlintConfig | null {
2248
return this.configForDirectory(path.dirname(filePath));
2349
}
2450

51+
/**
52+
* Give the path to a directory, find the closest `tsconfig.json` or
53+
* `jsconfig.json` file in the directory structure, including in the directory
54+
* itself, and load its configuration.
55+
*/
2556
public configForDirectory(directory: string): GlintConfig | null {
2657
let ts = findTypeScript(directory);
2758
if (!ts) return null;
2859

2960
let configPath = findNearestConfigFile(ts, directory);
3061
if (!configPath) return null;
3162

32-
let existing = this.configs.get(configPath);
63+
return this.configForConfigFile(configPath, ts);
64+
}
65+
66+
private configForConfigFile(configPath: string, tsArg?: TypeScript): GlintConfig | null {
67+
let ts = tsArg || findTypeScript(path.dirname(configPath));
68+
if (!ts) return null;
69+
70+
// Normalize the config path
71+
let absPath = path.resolve(configPath);
72+
73+
let existing = this.configs.get(absPath);
3374
if (existing !== undefined) return existing;
3475

35-
let configInput = loadConfigInput(ts, configPath);
36-
let config = configInput ? new GlintConfig(ts, configPath, configInput) : null;
76+
let configInput = loadConfigInput(ts, absPath);
77+
let config = configInput ? new GlintConfig(ts, absPath, configInput) : null;
3778

38-
this.configs.set(configPath, config);
79+
this.configs.set(absPath, config);
3980

4081
return config;
4182
}
@@ -61,6 +102,14 @@ function tryResolve<T>(load: () => T): T | null {
61102
}
62103
}
63104

105+
function fileExists(filePath: string): boolean {
106+
try {
107+
return fs.statSync(filePath).isFile();
108+
} catch (e) {
109+
return false;
110+
}
111+
}
112+
64113
function loadConfigInput(ts: TypeScript, entryPath: string): GlintConfigInput | null {
65114
let fullGlintConfig: Record<string, unknown> = {};
66115
let currentPath: string | undefined = entryPath;

packages/core/src/index.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GlintConfig, loadConfig } from './config/index.js';
1+
import { GlintConfig, loadClosestConfig, loadConfigFromProject } from './config/index.js';
22
import DocumentCache from './common/document-cache.js';
33
import TransformManager from './common/transform-manager.js';
44
import GlintLanguageServer from './language-server/glint-language-server.js';
@@ -25,8 +25,9 @@ export const pathUtils = utils;
2525
*
2626
* @internal
2727
*/
28-
export function analyzeProject(projectDirectory: string = process.cwd()): ProjectAnalysis {
29-
let glintConfig = loadConfig(projectDirectory);
28+
export function analyzeProject(from?: string): ProjectAnalysis {
29+
let glintConfig =
30+
from !== undefined ? loadConfigFromProject(from) : loadClosestConfig(process.cwd());
3031
let documents = new DocumentCache(glintConfig);
3132
let transformManager = new TransformManager(glintConfig, documents);
3233
let languageServer = new GlintLanguageServer(glintConfig, documents, transformManager);
@@ -40,6 +41,6 @@ export function analyzeProject(projectDirectory: string = process.cwd()): Projec
4041
};
4142
}
4243

43-
export { loadConfig };
44+
export { loadClosestConfig, loadConfigFromProject };
4445

4546
export type { TransformManager, GlintConfig, GlintLanguageServer };

0 commit comments

Comments
 (0)