diff --git a/packages/core/__tests__/config/load-config.test.ts b/packages/core/__tests__/config/load-config.test.ts index d147c88e5..ea4ac1af5 100644 --- a/packages/core/__tests__/config/load-config.test.ts +++ b/packages/core/__tests__/config/load-config.test.ts @@ -3,7 +3,7 @@ import * as os from 'node:os'; import { describe, beforeEach, afterEach, test, expect, vi } from 'vitest'; import { loadConfig } from '../../src/config/index.js'; import { normalizePath } from '../../src/config/config.js'; -import { require } from '../../src/config/loader.js'; +import { findTypeScript, loadConfigInput, require } from '../../src/config/loader.js'; describe('Config: loadConfig', () => { const testDir = `${os.tmpdir()}/glint-config-test-load-config-${process.pid}`; @@ -51,6 +51,87 @@ describe('Config: loadConfig', () => { expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: {} }); }); + test('recursively extends config', () => { + fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true }); + fs.mkdirSync(`${testDir}/other`, { recursive: true }); + + fs.writeFileSync( + `${testDir}/tsconfig.json`, + JSON.stringify({ + extends: './other/tsconfig.json', + glint: { + environment: '../local-env', + }, + }), + ); + fs.writeFileSync( + `${testDir}/other/tsconfig.json`, + JSON.stringify({ + glint: { + environment: 'kaboom', + }, + }), + ); + + fs.writeFileSync( + `${testDir}/deeply/tsconfig.json`, + JSON.stringify({ + extends: '../tsconfig.json', + }), + ); + + let ts = findTypeScript(`${testDir}/deeply`); + if (!ts) { + expect.fail('TypeScript not found'); + } + let glintConfig = loadConfigInput(ts, `${testDir}/deeply/tsconfig.json`); + expect(glintConfig).toEqual({ environment: '../local-env' }); + + let config = loadConfig(`${testDir}/deeply/nested/directory`); + expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`)); + }); + + test('extends multiple parents', () => { + fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true }); + fs.mkdirSync(`${testDir}/other`, { recursive: true }); + + fs.writeFileSync( + `${testDir}/tsconfig.json`, + JSON.stringify({ + glint: { + environment: 'kaboom', + checkStandaloneTemplates: true, + }, + }), + ); + fs.writeFileSync( + `${testDir}/other/tsconfig.json`, + JSON.stringify({ + glint: { + environment: '../local-env', + }, + }), + ); + + fs.writeFileSync( + `${testDir}/deeply/tsconfig.json`, + JSON.stringify({ + extends: ['../tsconfig.json', '../other/tsconfig.json'], + }), + ); + + let ts = findTypeScript(`${testDir}/deeply`); + if (!ts) { + expect.fail('TypeScript not found'); + } + let glintConfig = loadConfigInput(ts, `${testDir}/deeply/tsconfig.json`); + expect(glintConfig).toEqual({ environment: '../local-env', checkStandaloneTemplates: true }); + + let config = loadConfig(`${testDir}/deeply/nested/directory`); + expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`)); + expect(config.environment.names).toEqual(['../local-env']); + }); + test('locates config in package', () => { const directory = `${testDir}/package-glint-config`; const nodeModulePackageDir = `${directory}/node_modules/@package1`; diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index cc32d6804..798dcdcd8 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -67,36 +67,43 @@ function tryResolve(load: () => T): T | null { } } -function loadConfigInput(ts: TypeScript, entryPath: string): GlintConfigInput | null { - let fullGlintConfig: Record = {}; - let currentPath: string | undefined = entryPath; +function parseConfigInput( + ts: TypeScript, + entryPath: string, + currentPath: string, + fullGlintConfig: Record, +): Record { + let currentContents: any = ts.readConfigFile(currentPath, ts.sys.readFile).config; + let currentGlintConfig = currentContents.glint ?? {}; - while (currentPath) { - let currentContents: any = ts.readConfigFile(currentPath, ts.sys.readFile).config; - let currentGlintConfig = currentContents.glint ?? {}; - - assert( - currentPath === entryPath || !currentGlintConfig.transform, - 'Glint `transform` options may not be specified in extended config.', - ); - - fullGlintConfig = { ...currentGlintConfig, ...fullGlintConfig }; + assert( + currentPath === entryPath || !currentGlintConfig.transform, + 'Glint `transform` options may not be specified in extended config.', + ); - if (currentContents.extends) { - currentPath = path.resolve(path.dirname(currentPath), currentContents.extends); - if (!fs.existsSync(currentPath)) { + if (currentContents.extends) { + let paths: string[] = Array.isArray(currentContents.extends) + ? currentContents.extends + : [currentContents.extends]; + for (let extendPath of paths) { + let currentExtendPath = path.resolve(path.dirname(currentPath), extendPath); + if (!fs.existsSync(currentExtendPath)) { try { - currentPath = require.resolve(currentContents.extends); + currentExtendPath = require.resolve(currentContents.extends); } catch { // suppress the exception thrown by require.resolve for those scenarios where the file does not exist } } - } else { - currentPath = undefined; + + fullGlintConfig = parseConfigInput(ts, entryPath, currentExtendPath, fullGlintConfig); } } - return validateConfigInput(fullGlintConfig); + return { ...fullGlintConfig, ...currentGlintConfig }; +} + +export function loadConfigInput(ts: TypeScript, entryPath: string): GlintConfigInput | null { + return validateConfigInput(parseConfigInput(ts, entryPath, entryPath, {})); } function findNearestConfigFile(ts: TypeScript, searchFrom: string): string {