Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix project argument to match tsc #709

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
145 changes: 117 additions & 28 deletions packages/core/__tests__/config/load-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import { describe, beforeEach, afterEach, test, expect } from 'vitest';
import { loadConfig } from '../../src/config/index.js';
import { loadClosestConfig, loadConfigFromProject } from '../../src/config/index.js';
import { normalizePath } from '../../src/config/config.js';

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

beforeEach(() => {
Expand All @@ -20,35 +20,124 @@ describe('Config: loadConfig', () => {
fs.rmSync(testDir, { recursive: true, force: true });
});

test('throws an error if no config is found', () => {
expect(() => loadConfig(testDir)).toThrow(`Unable to find Glint configuration for ${testDir}`);
describe('loadClosestConfig', () => {
test('throws an error if no config is found', () => {
expect(() => loadClosestConfig(testDir)).toThrow(
`Unable to find Glint configuration for ${testDir}`
);
});

test('loads from a folder', () => {
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: {
environment: './local-env',
},
})
);

let config = loadClosestConfig(`${testDir}/deeply/nested/directory`);

expect(config.rootDir).toBe(normalizePath(testDir));
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
});

test('locates config in a parent directory', () => {
fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true });
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: {
environment: 'kaboom',
checkStandaloneTemplates: false,
},
})
);
fs.writeFileSync(
`${testDir}/deeply/tsconfig.json`,
JSON.stringify({
extends: '../tsconfig.json',
glint: {
environment: '../local-env',
},
})
);

let config = loadClosestConfig(`${testDir}/deeply/nested/directory`);

expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`));
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
expect(config.checkStandaloneTemplates).toBe(false);
});
});

test('locates config in a parent directory', () => {
fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true });
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: {
environment: 'kaboom',
checkStandaloneTemplates: false,
},
})
);
fs.writeFileSync(
`${testDir}/deeply/tsconfig.json`,
JSON.stringify({
extends: '../tsconfig.json',
glint: {
environment: '../local-env',
},
})
);
describe('loadConfigFromProject', () => {
test('throws an error if no config is found', () => {
expect(() => loadConfigFromProject(testDir)).toThrow(
`Unable to find Glint configuration for project ${testDir}`
);
expect(() => loadConfigFromProject(`${testDir}/tsconfig.json`)).toThrow(
`Unable to find Glint configuration for project ${testDir}`
);
});

test('loads from a tsconfig.json in a folder', () => {
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: {
environment: './local-env',
},
})
);

expect(loadConfigFromProject(testDir).rootDir).toBe(normalizePath(testDir));
});

test('loads from a jsconfig.json in a folder', () => {
fs.writeFileSync(
`${testDir}/jsconfig.json`,
JSON.stringify({
glint: {
environment: './local-env',
},
})
);

expect(loadConfigFromProject(testDir).rootDir).toBe(normalizePath(testDir));
});

test('loads from a file', () => {
fs.writeFileSync(
`${testDir}/tsconfig.custom.json`,
JSON.stringify({
glint: {
environment: './local-env',
},
})
);

expect(loadConfigFromProject(`${testDir}/tsconfig.custom.json`).rootDir).toBe(
normalizePath(testDir)
);
});

let config = loadConfig(`${testDir}/deeply/nested/directory`);
test('does not search parent directories', () => {
fs.mkdirSync(`${testDir}/sub`, { recursive: true });
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: {
environment: 'kaboom',
checkStandaloneTemplates: false,
},
})
);

expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`));
expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} });
expect(config.checkStandaloneTemplates).toBe(false);
expect(() => loadConfigFromProject(`${testDir}/sub`)).toThrow(
`Unable to find Glint configuration for project ${testDir}`
);
});
});
});
42 changes: 42 additions & 0 deletions packages/core/__tests__/config/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,50 @@ describe('Config: loadConfig', () => {
let loader = new ConfigLoader();
let configA = loader.configForFile(`${testDir}/src/a.ts`);
let configB = loader.configForFile(`${testDir}/src/b.ts`);
let configC = loader.configForFile(`${testDir}/src/../src/c.ts`);

expect(configA).toBe(configB);
expect(configA).toBe(configC);
});

test('returns config from project file path', () => {
fs.writeFileSync(
`${testDir}/tsconfig.customname.json`,
JSON.stringify({
glint: { environment: './local-env.js' },
})
);

expect(
new ConfigLoader().configForProjectPath(`${testDir}/tsconfig.customname.json`)?.rootDir
).toBe(normalizePath(`${testDir}`));
});

test('returns config from project folder path', () => {
fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: { environment: './local-env.js' },
})
);

expect(new ConfigLoader().configForProjectPath(testDir)?.rootDir).toBe(
normalizePath(`${testDir}`)
);
});

test('returns null for invalid project paths', () => {
fs.mkdirSync(`${testDir}/packages/a/src`, { recursive: true });

fs.writeFileSync(
`${testDir}/tsconfig.json`,
JSON.stringify({
glint: { environment: './local-env.js' },
})
);

expect(new ConfigLoader().configForProjectPath(`${testDir}/tsconfig.missing.json`)).toBeNull();
expect(new ConfigLoader().configForProjectPath(`${testDir}/packages/a/src`)).toBeNull();
});

describe('extending other config', () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRequire } from 'node:module';
import yargs from 'yargs';
import { findTypeScript, loadConfig } from '../config/index.js';
import { findTypeScript, loadClosestConfig, loadConfigFromProject } from '../config/index.js';
import { performWatch } from './perform-watch.js';
import { performCheck } from './perform-check.js';
import { determineOptionsToExtend } from './options.js';
Expand All @@ -18,7 +18,7 @@ const argv = yargs(process.argv.slice(2))
.option('project', {
alias: 'p',
string: true,
description: 'The path to the tsconfig file to use',
description: 'The path to the tsconfig file to use or the folder containing it',
})
.option('watch', {
alias: 'w',
Expand Down Expand Up @@ -120,7 +120,8 @@ if (argv.build) {
performBuild(ts, projects, buildOptions);
}
} else {
const glintConfig = loadConfig(argv.project ?? cwd);
const glintConfig =
argv.project !== undefined ? loadConfigFromProject(argv.project) : loadClosestConfig(cwd);
const optionsToExtend = determineOptionsToExtend(argv);

validateTSOrExit(glintConfig.ts);
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,26 @@ export { GlintConfig } from './config.js';
export { GlintEnvironment } from './environment.js';
export { ConfigLoader, findTypeScript } from './loader.js';

/**
* Loads glint configuration from the specified project path. If a path to a
* file is passed, the config is loaded from that file. If the path to a folder
* is passed, the config is loaded from the `tsconfig.json` or `jsconfig.json`
* file contained in that folder. Raises an error if no configuration is found.
*/
export function loadConfigFromProject(from: string): GlintConfig {
let config = new ConfigLoader().configForProjectPath(from);
if (!config) {
throw new SilentError(`Unable to find Glint configuration for project ${from}`);
}
return config;
}

/**
* Loads glint configuration, starting from the given directory
* and searching upwards and raising an error if no configuration
* is found.
*/
export function loadConfig(from: string): GlintConfig {
export function loadClosestConfig(from: string): GlintConfig {
let config = findConfig(from);
if (!config) {
throw new SilentError(`Unable to find Glint configuration for ${from}`);
Expand Down
65 changes: 57 additions & 8 deletions packages/core/src/config/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRequire } from 'node:module';
import * as path from 'node:path';
import * as fs from 'node:fs';
import SilentError from 'silent-error';
import { GlintConfig } from './config.js';
import { GlintConfigInput } from '@glint/core/config-types';
Expand All @@ -10,32 +11,72 @@ const require = createRequire(import.meta.url);
type TypeScript = typeof TS;

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

/**
* Given the path to a configuration file, or to a folder containing a
* `tsconfig.json` or `jsconfig.json`, load the configuration. This is meant
* to implement the behavior of `glint`/`tsc`'s `--project` command-line
* option.
*/
public configForProjectPath(configPath: string): GlintConfig | null {
let tsConfigPath = path.join(configPath, 'tsconfig.json');
let jsConfigPath = path.join(configPath, 'jsconfig.json');

if (fileExists(configPath)) {
return this.configForConfigFile(configPath);
} else if (fileExists(tsConfigPath)) {
return this.configForConfigFile(tsConfigPath);
} else if (fileExists(jsConfigPath)) {
return this.configForConfigFile(jsConfigPath);
} else {
return null;
}
}

/**
* Given the path to a file, find the closest `tsconfig.json` or
* `jsconfig.json` file in the directory structure and load its configuration.
*/
public configForFile(filePath: string): GlintConfig | null {
return this.configForDirectory(path.dirname(filePath));
}

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

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

let existing = this.configs.get(configPath);
return this.configForConfigFile(configPath, ts);
}

private configForConfigFile(configPath: string, tsArg?: TypeScript): GlintConfig | null {
let ts = tsArg || findTypeScript(path.dirname(configPath));
if (!ts) return null;

// Normalize the config path
let absPath = path.resolve(configPath);

let existing = this.configs.get(absPath);
if (existing !== undefined) return existing;

let configInput = loadConfigInput(ts, configPath);
let config = configInput ? new GlintConfig(ts, configPath, configInput) : null;
let configInput = loadConfigInput(ts, absPath);
let config = configInput ? new GlintConfig(ts, absPath, configInput) : null;

this.configs.set(configPath, config);
this.configs.set(absPath, config);

return config;
}
Expand All @@ -61,6 +102,14 @@ function tryResolve<T>(load: () => T): T | null {
}
}

function fileExists(filePath: string): boolean {
try {
return fs.statSync(filePath).isFile();
} catch (e) {
return false;
}
}

function loadConfigInput(ts: TypeScript, entryPath: string): GlintConfigInput | null {
let fullGlintConfig: Record<string, unknown> = {};
let currentPath: string | undefined = entryPath;
Expand Down
Loading