diff --git a/cspell.code-workspace b/cspell.code-workspace index 7f61323d5a2..d60de74a740 100644 --- a/cspell.code-workspace +++ b/cspell.code-workspace @@ -43,6 +43,9 @@ "autoAttachChildProcesses": true, "skipFiles": ["/**", "**/node_modules/**"], "program": "${workspaceRoot:cspell-monorepo}/node_modules/vitest/vitest.mjs", + "env": { + "NODE_OPTIONS": "--enable-source-maps" + }, "args": [ "run", "--testTimeout=600000", diff --git a/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.test.ts b/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.test.ts index 4cce773a098..f9943749cf0 100644 --- a/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.test.ts +++ b/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.test.ts @@ -4,7 +4,7 @@ import type { CSpellSettings } from '@cspell/cspell-types'; import { describe, expect, test, vi } from 'vitest'; import { CSpellConfigFile } from './CSpellConfigFile.js'; -import { CSpellConfigFileJavaScript } from './CSpellConfigFile/index.js'; +import { CSpellConfigFileInMemory, CSpellConfigFileJavaScript } from './CSpellConfigFile/index.js'; import { CSpellConfigFileReaderWriterImpl } from './CSpellConfigFileReaderWriter.js'; import type { IO } from './IO.js'; import { defaultLoaders } from './loaders/index.js'; @@ -16,8 +16,8 @@ const oc = (obj: T) => expect.objectContaining(obj); describe('CSpellConfigFileReaderWriter', () => { test.each` - uri | content | expected - ${'file:///package.json'} | ${json({ name: 'name' })} | ${oc({ url: new URL('file:///package.json'), settings: {} })} + uri | content | expected + ${'file:///package.json'} | ${json({ name: 'name', cspell: { words: ['one'] } })} | ${oc({ url: new URL('file:///package.json'), settings: { words: ['one'] } })} `('readConfig', async ({ uri, content, expected }) => { const io: IO = { readFile: vi.fn((url) => Promise.resolve({ url, content })), @@ -136,6 +136,34 @@ describe('CSpellConfigFileReaderWriter', () => { await expect(rw.readConfig(uri)).resolves.toEqual(expected); }); + + test.each` + url | content + ${'file:///cspell.json'} | ${json({ name: 'name', words: ['one'] })} + `('toCSpellConfigFile $url', async ({ url, content }) => { + const io: IO = { + readFile: vi.fn((url) => Promise.resolve({ url, content })), + writeFile: vi.fn(), + }; + url = new URL(url); + const settings = JSON.parse(content) as CSpellSettings; + const rw = new CSpellConfigFileReaderWriterImpl(io, defaultDeserializers, defaultLoaders); + const config = await rw.readConfig(url); + expect(config).toBeInstanceOf(CSpellConfigFile); + expect(config.url).toEqual(url); + expect(config.settings).toEqual(settings); + + expect(rw.toCSpellConfigFile(config)).toBe(config); + + const config2 = rw.toCSpellConfigFile({ url, settings }); + expect(config2).toBeInstanceOf(CSpellConfigFile); + expect(config2).not.toEqual(config); + + // At the moment, we do not try to associate the settings with the right loader. + expect(config2).toBeInstanceOf(CSpellConfigFileInMemory); + + expect(config2.settings).toEqual(settings); + }); }); class Cfg extends CSpellConfigFile { diff --git a/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.ts b/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.ts index f7a0402dd90..f552e971e3b 100644 --- a/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.ts +++ b/packages/cspell-config-lib/src/CSpellConfigFileReaderWriter.ts @@ -1,6 +1,8 @@ import { extname } from 'node:path/posix'; -import type { CSpellConfigFile, ICSpellConfigFile } from './CSpellConfigFile.js'; +import type { ICSpellConfigFile } from './CSpellConfigFile.js'; +import { CSpellConfigFile } from './CSpellConfigFile.js'; +import { CSpellConfigFileInMemory } from './CSpellConfigFile/index.js'; import type { FileLoaderMiddleware } from './FileLoader.js'; import type { IO } from './IO.js'; import { getDeserializer, getLoader, getSerializer } from './middlewareHelper.js'; @@ -17,6 +19,7 @@ export interface CSpellConfigFileReaderWriter { clearCachedFiles(): void; setUntrustedExtensions(ext: readonly string[]): this; setTrustedUrls(urls: readonly (URL | string)[]): this; + toCSpellConfigFile(configFile: ICSpellConfigFile): CSpellConfigFile; /** * Untrusted extensions are extensions that are not trusted to be loaded from a file system. * Extension are case insensitive and should include the leading dot. @@ -68,6 +71,12 @@ export class CSpellConfigFileReaderWriterImpl implements CSpellConfigFileReaderW return loader({ url: toURL(uri), context: { deserialize: this.getDeserializer(), io: this.io } }); } + toCSpellConfigFile(configFile: ICSpellConfigFile): CSpellConfigFile { + return configFile instanceof CSpellConfigFile + ? configFile + : new CSpellConfigFileInMemory(configFile.url, configFile.settings); + } + getDeserializer(): DeserializerNext { return getDeserializer(this.middleware); } diff --git a/packages/cspell-config-lib/src/index.ts b/packages/cspell-config-lib/src/index.ts index 4540d4c3f7f..3686ea08b6b 100644 --- a/packages/cspell-config-lib/src/index.ts +++ b/packages/cspell-config-lib/src/index.ts @@ -1,4 +1,5 @@ export { createReaderWriter } from './createReaderWriter.js'; +export type { ICSpellConfigFile } from './CSpellConfigFile.js'; export { CSpellConfigFile } from './CSpellConfigFile.js'; export { CSpellConfigFileInMemory, diff --git a/packages/cspell-lib/api/api.d.ts b/packages/cspell-lib/api/api.d.ts index 193d4ef1a37..4f66d139c6a 100644 --- a/packages/cspell-lib/api/api.d.ts +++ b/packages/cspell-lib/api/api.d.ts @@ -9,7 +9,7 @@ import { SpellingDictionaryCollection, SuggestOptions, SuggestionResult, Caching export { SpellingDictionary, SpellingDictionaryCollection, SuggestOptions, SuggestionCollector, SuggestionResult, createSpellingDictionary, createCollection as createSpellingDictionaryCollection } from 'cspell-dictionary'; import { WeightMap } from 'cspell-trie-lib'; export { CompoundWordsMethod } from 'cspell-trie-lib'; -import { CSpellConfigFile } from 'cspell-config-lib'; +import { CSpellConfigFile, ICSpellConfigFile } from 'cspell-config-lib'; /** * Clear the cached files and other cached data. @@ -374,6 +374,12 @@ interface IConfigLoader { * @param settings - settings to use. */ createCSpellConfigFile(filename: URL | string, settings: CSpellUserSettings): CSpellConfigFile; + /** + * Convert a ICSpellConfigFile into a CSpellConfigFile. + * If cfg is a CSpellConfigFile, it is returned as is. + * @param cfg - configuration file to convert. + */ + toCSpellConfigFile(cfg: ICSpellConfigFile): CSpellConfigFile; /** * Unsubscribe from any events and dispose of any resources including caches. */ @@ -401,6 +407,8 @@ declare function searchForConfig(searchFrom: URL | string | undefined, pnpSettin * @returns normalized CSpellSettings */ declare function loadConfig(file: string, pnpSettings?: PnPSettingsOptional): Promise; +declare function readConfigFile(filename: string | URL, relativeTo?: string | URL): Promise; +declare function resolveConfigFileImports(configFile: CSpellConfigFile | ICSpellConfigFile): Promise; /** * Might throw if the settings have not yet been loaded. * @deprecated use {@link getGlobalSettingsAsync} instead. @@ -1057,4 +1065,4 @@ declare namespace textApi_d { export { textApi_d_calculateTextDocumentOffsets as calculateTextDocumentOffsets, textApi_d_camelToSnake as camelToSnake, textApi_d_cleanText as cleanText, textApi_d_cleanTextOffset as cleanTextOffset, textApi_d_extractLinesOfText as extractLinesOfText, textApi_d_extractPossibleWordsFromTextOffset as extractPossibleWordsFromTextOffset, textApi_d_extractText as extractText, textApi_d_extractWordsFromCode as extractWordsFromCode, textApi_d_extractWordsFromCodeTextOffset as extractWordsFromCodeTextOffset, textApi_d_extractWordsFromText as extractWordsFromText, textApi_d_extractWordsFromTextOffset as extractWordsFromTextOffset, textApi_d_isFirstCharacterLower as isFirstCharacterLower, textApi_d_isFirstCharacterUpper as isFirstCharacterUpper, textApi_d_isLowerCase as isLowerCase, textApi_d_isUpperCase as isUpperCase, textApi_d_lcFirst as lcFirst, textApi_d_match as match, textApi_d_matchCase as matchCase, textApi_d_matchStringToTextOffset as matchStringToTextOffset, textApi_d_matchToTextOffset as matchToTextOffset, textApi_d_removeAccents as removeAccents, textApi_d_snakeToCamel as snakeToCamel, textApi_d_splitCamelCaseWord as splitCamelCaseWord, textApi_d_splitCamelCaseWordWithOffset as splitCamelCaseWordWithOffset, textApi_d_stringToRegExp as stringToRegExp, textApi_d_textOffset as textOffset, textApi_d_ucFirst as ucFirst }; } -export { type CheckTextInfo, type ConfigurationDependencies, type CreateTextDocumentParams, type DetermineFinalDocumentSettingsResult, type Document, DocumentValidator, type DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, type ExcludeFilesGlobMap, type ExclusionFunction, exclusionHelper_d as ExclusionHelper, type FeatureFlag, FeatureFlags, ImportError, type ImportFileRefWithError$1 as ImportFileRefWithError, IncludeExcludeFlag, type IncludeExcludeOptions, index_link_d as Link, type Logger, type PerfTimer, type SpellCheckFileOptions, type SpellCheckFilePerf, type SpellCheckFileResult, SpellingDictionaryLoadError, type SuggestedWord, SuggestionError, type SuggestionOptions, type SuggestionsForWordResult, textApi_d as Text, type TextDocument, type TextDocumentLine, type TextDocumentRef, type TextInfoItem, type TraceOptions, type TraceResult, type TraceWordResult, UnknownFeatureFlagError, type ValidationIssue, calcOverrideSettings, checkFilenameMatchesExcludeGlob as checkFilenameMatchesGlob, checkText, checkTextDocument, clearCachedFiles, clearCaches, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createConfigLoader, createPerfTimer, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, fileToTextDocument, finalizeSettings, getCachedFileSize, getDefaultBundledSettingsAsync, getDefaultConfigLoader, getDefaultSettings, getDictionary, getGlobalSettings, getGlobalSettingsAsync, getLogger, getSources, getSystemFeatureFlags, getVirtualFS, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, mergeInDocSettings, mergeSettings, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveFile, searchForConfig, sectionCSpell, setLogger, shouldCheckDocument, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, updateTextDocument, validateText }; +export { type CheckTextInfo, type ConfigurationDependencies, type CreateTextDocumentParams, type DetermineFinalDocumentSettingsResult, type Document, DocumentValidator, type DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, type ExcludeFilesGlobMap, type ExclusionFunction, exclusionHelper_d as ExclusionHelper, type FeatureFlag, FeatureFlags, ImportError, type ImportFileRefWithError$1 as ImportFileRefWithError, IncludeExcludeFlag, type IncludeExcludeOptions, index_link_d as Link, type Logger, type PerfTimer, type SpellCheckFileOptions, type SpellCheckFilePerf, type SpellCheckFileResult, SpellingDictionaryLoadError, type SuggestedWord, SuggestionError, type SuggestionOptions, type SuggestionsForWordResult, textApi_d as Text, type TextDocument, type TextDocumentLine, type TextDocumentRef, type TextInfoItem, type TraceOptions, type TraceResult, type TraceWordResult, UnknownFeatureFlagError, type ValidationIssue, calcOverrideSettings, checkFilenameMatchesExcludeGlob as checkFilenameMatchesGlob, checkText, checkTextDocument, clearCachedFiles, clearCaches, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createConfigLoader, createPerfTimer, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, fileToTextDocument, finalizeSettings, getCachedFileSize, getDefaultBundledSettingsAsync, getDefaultConfigLoader, getDefaultSettings, getDictionary, getGlobalSettings, getGlobalSettingsAsync, getLogger, getSources, getSystemFeatureFlags, getVirtualFS, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, mergeInDocSettings, mergeSettings, readConfigFile, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveConfigFileImports, resolveFile, searchForConfig, sectionCSpell, setLogger, shouldCheckDocument, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, updateTextDocument, validateText }; diff --git a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/configLoader.ts b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/configLoader.ts index 7dd7d3c0698..81d69116b50 100644 --- a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/configLoader.ts +++ b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/configLoader.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { CSpellUserSettings, ImportFileRef, Source } from '@cspell/cspell-types'; -import type { CSpellConfigFile, CSpellConfigFileReaderWriter, IO, TextFile } from 'cspell-config-lib'; -import { createReaderWriter, CSpellConfigFileInMemory } from 'cspell-config-lib'; +import { CSpellConfigFile, CSpellConfigFileReaderWriter, ICSpellConfigFile, IO, TextFile } from 'cspell-config-lib'; +import { createReaderWriter } from 'cspell-config-lib'; import { isUrlLike, toFileURL } from 'cspell-io'; import { URI, Utils as UriUtils } from 'vscode-uri'; @@ -154,6 +154,13 @@ export interface IConfigLoader { */ createCSpellConfigFile(filename: URL | string, settings: CSpellUserSettings): CSpellConfigFile; + /** + * Convert a ICSpellConfigFile into a CSpellConfigFile. + * If cfg is a CSpellConfigFile, it is returned as is. + * @param cfg - configuration file to convert. + */ + toCSpellConfigFile(cfg: ICSpellConfigFile): CSpellConfigFile; + /** * Unsubscribe from any events and dispose of any resources including caches. */ @@ -204,7 +211,7 @@ export class ConfigLoader implements IConfigLoader { protected cachedConfigFiles = new Map(); protected cachedPendingConfigFile = new AutoResolveCache>(); protected cachedMergedConfig = new WeakMap(); - protected cachedCSpellConfigFileInMemory = new WeakMap>(); + protected cachedCSpellConfigFileInMemory = new WeakMap>(); protected globalSettings: CSpellSettingsI | undefined; protected cspellConfigFileReaderWriter: CSpellConfigFileReaderWriter; protected configSearch: ConfigSearch; @@ -435,10 +442,11 @@ export class ConfigLoader implements IConfigLoader { } public mergeConfigFileWithImports( - cfgFile: CSpellConfigFile, + cfg: CSpellConfigFile | ICSpellConfigFile, pnpSettings: PnPSettingsOptional | undefined, referencedBy?: string[] | undefined, ): Promise { + const cfgFile = this.toCSpellConfigFile(cfg); const cached = this.cachedMergedConfig.get(cfgFile); if (cached && cached.pnpSettings === pnpSettings && cached.referencedBy === referencedBy) { return cached.result; @@ -540,8 +548,19 @@ export class ConfigLoader implements IConfigLoader { } createCSpellConfigFile(filename: URL | string, settings: CSpellUserSettings): CSpellConfigFile { - const map = autoResolveWeak(this.cachedCSpellConfigFileInMemory, settings, () => new Map()); - return autoResolve(map, filename, () => new CSpellConfigFileInMemory(toFileURL(filename), settings)); + const map = autoResolveWeak( + this.cachedCSpellConfigFileInMemory, + settings, + () => new Map(), + ); + return autoResolve(map, filename, () => + this.cspellConfigFileReaderWriter.toCSpellConfigFile({ url: toFileURL(filename), settings }), + ); + } + + toCSpellConfigFile(cfg: ICSpellConfigFile): CSpellConfigFile { + if (cfg instanceof CSpellConfigFile) return cfg; + return this.createCSpellConfigFile(cfg.url, cfg.settings); } dispose() { diff --git a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.test.ts b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.test.ts index 6387793fac7..67fa82b432c 100644 --- a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.test.ts +++ b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.test.ts @@ -1,9 +1,19 @@ import { describe, expect, test } from 'vitest'; -import { getDefaultConfigLoader } from './defaultConfigLoader.js'; +import { getDefaultConfigLoader, resolveConfigFileImports } from './defaultConfigLoader.js'; describe('defaultConfigLoader', () => { test('getDefaultConfigLoader', () => { expect(getDefaultConfigLoader()).toEqual(expect.any(Object)); }); + + test('resolveConfigFileImports', async () => { + const configFile = { + url: new URL('cspell.json', import.meta.url), + settings: { + words: ['one'], + }, + }; + expect(await resolveConfigFileImports(configFile)).toEqual(expect.objectContaining({ words: ['one'] })); + }); }); diff --git a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.ts b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.ts index 224a156294a..a9668d28c11 100644 --- a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.ts +++ b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/defaultConfigLoader.ts @@ -1,5 +1,5 @@ import type { CSpellSettings } from '@cspell/cspell-types'; -import type { CSpellConfigFile } from 'cspell-config-lib'; +import type { CSpellConfigFile, ICSpellConfigFile } from 'cspell-config-lib'; import { toError } from '../../../util/errors.js'; import { toFileUrl } from '../../../util/url.js'; @@ -56,6 +56,12 @@ export async function readConfigFile(filename: string | URL, relativeTo?: string return result; } +export async function resolveConfigFileImports( + configFile: CSpellConfigFile | ICSpellConfigFile, +): Promise { + return gcl().mergeConfigFileWithImports(configFile, configFile.settings); +} + /** * Might throw if the settings have not yet been loaded. * @deprecated use {@link getGlobalSettingsAsync} instead. @@ -83,9 +89,11 @@ export function clearCachedSettingsFiles(): void { export function getDefaultConfigLoader(): IConfigLoader { return getDefaultConfigLoaderInternal(); } + function cachedFiles() { return gcl()._cachedFiles; } + export async function readRawSettings(filename: string | URL, relativeTo?: string | URL): Promise { try { const cfg = await readConfigFile(filename, relativeTo); diff --git a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/index.ts b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/index.ts index 24770d1be5c..5074fca9347 100644 --- a/packages/cspell-lib/src/lib/Settings/Controller/configLoader/index.ts +++ b/packages/cspell-lib/src/lib/Settings/Controller/configLoader/index.ts @@ -14,7 +14,9 @@ export { getGlobalSettings, getGlobalSettingsAsync, loadConfig, + readConfigFile, readRawSettings, + resolveConfigFileImports, resolveSettingsImports, searchForConfig, } from './defaultConfigLoader.js'; diff --git a/packages/cspell-lib/src/lib/Settings/index.ts b/packages/cspell-lib/src/lib/Settings/index.ts index 7c2ab6253eb..5a616973cb5 100644 --- a/packages/cspell-lib/src/lib/Settings/index.ts +++ b/packages/cspell-lib/src/lib/Settings/index.ts @@ -13,9 +13,11 @@ export { getGlobalSettingsAsync, loadConfig, loadPnP, + readConfigFile, readRawSettings, readSettings, readSettingsFiles, + resolveConfigFileImports, resolveSettingsImports, searchForConfig, sectionCSpell, diff --git a/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap b/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap index 2186256d0b2..617e1b54902 100644 --- a/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap +++ b/packages/cspell-lib/src/lib/__snapshots__/index.test.ts.snap @@ -253,12 +253,14 @@ exports[`Validate the cspell API > Verify API exports 1`] = ` "loadPnP": [Function], "mergeInDocSettings": [Function], "mergeSettings": [Function], + "readConfigFile": [Function], "readFile": [Function], "readFileSync": [Function], "readRawSettings": [Function], "readSettings": [Function], "readSettingsFiles": [Function], "refreshDictionaryCache": [Function], + "resolveConfigFileImports": [Function], "resolveFile": [Function], "searchForConfig": [Function], "sectionCSpell": "cSpell", diff --git a/packages/cspell-lib/src/lib/index.ts b/packages/cspell-lib/src/lib/index.ts index 88a827f4967..7df83a12073 100644 --- a/packages/cspell-lib/src/lib/index.ts +++ b/packages/cspell-lib/src/lib/index.ts @@ -42,9 +42,11 @@ export { loadPnP, mergeInDocSettings, mergeSettings, + readConfigFile, readRawSettings, readSettings, readSettingsFiles, + resolveConfigFileImports, searchForConfig, sectionCSpell, } from './Settings/index.js'; diff --git a/packages/cspell/src/app/lint/LintRequest.ts b/packages/cspell/src/app/lint/LintRequest.ts index df8159fc9f6..b71faff398b 100644 --- a/packages/cspell/src/app/lint/LintRequest.ts +++ b/packages/cspell/src/app/lint/LintRequest.ts @@ -2,7 +2,7 @@ import * as path from 'node:path'; import type { Issue } from '@cspell/cspell-types'; -import type { LinterCliOptions, LinterOptions } from '../options.js'; +import type { CSpellConfigFile, LinterCliOptions, LinterOptions } from '../options.js'; import type { GlobSrcInfo } from '../util/glob.js'; import { calcExcludeGlobInfo } from '../util/glob.js'; import type { FinalizedReporter } from '../util/reporters.js'; @@ -18,7 +18,7 @@ export class LintRequest { readonly uniqueFilter: (issue: Issue) => boolean; readonly locale: string; - readonly configFile: string | undefined; + readonly configFile: string | CSpellConfigFile | undefined; readonly excludes: GlobSrcInfo[]; readonly root: string; readonly showContext: number; diff --git a/packages/cspell/src/app/options.ts b/packages/cspell/src/app/options.ts index 07cfdf4de6b..8174e3b5ffe 100644 --- a/packages/cspell/src/app/options.ts +++ b/packages/cspell/src/app/options.ts @@ -1,6 +1,8 @@ +import { CSpellSettings } from '@cspell/cspell-types'; + import type { CacheOptions } from './util/cache/index.js'; -export interface LinterOptions extends BaseOptions, Omit { +export interface LinterOptions extends Omit, Omit { /** * Display verbose information */ @@ -93,10 +95,16 @@ export interface LinterOptions extends BaseOptions, Omit; + export interface BaseOptions { /** * Path to configuration file. @@ -265,10 +275,15 @@ export interface LinterCliOptions extends LinterOptions { issueTemplate?: string; } -export function fixLegacy(opts: T & LegacyOptions): Omit { +export function fixLegacy(opts: T & LegacyOptions): Omit { const { local, ...rest } = opts; if (local && !rest.locale) { rest.locale = local; } return rest; } + +export interface CSpellConfigFile { + url: URL; + settings: CSpellSettings; +} diff --git a/packages/cspell/src/app/util/fileHelper.ts b/packages/cspell/src/app/util/fileHelper.ts index bfd1359a3af..45449da30d0 100644 --- a/packages/cspell/src/app/util/fileHelper.ts +++ b/packages/cspell/src/app/util/fileHelper.ts @@ -2,7 +2,7 @@ import { promises as fsp } from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { toFileDirURL, toFileURL } from '@cspell/url'; +import { toFileDirURL, toFilePathOrHref, toFileURL } from '@cspell/url'; import type { BufferEncoding } from 'cspell-io'; import { readFileText as cioReadFile, toURL } from 'cspell-io'; import type { CSpellUserSettings, Document, Issue } from 'cspell-lib'; @@ -10,6 +10,7 @@ import * as cspell from 'cspell-lib'; import { fileToDocument, isBinaryFile as isUriBinaryFile } from 'cspell-lib'; import getStdin from 'get-stdin'; +import { CSpellConfigFile } from '../options.js'; import { asyncAwait, asyncFlatten, asyncMap, asyncPipe, mergeAsyncIterables } from './async.js'; import { FileUrlPrefix, STDIN, STDINProtocol, STDINUrlPrefix, UTF8 } from './constants.js'; import { IOError, toApplicationError, toError } from './errors.js'; @@ -31,15 +32,38 @@ export interface FileConfigInfo { languageIds: string[]; } -export async function readConfig(configFile: string | undefined, root: string | undefined): Promise { +export async function readConfig( + configFile: string | CSpellConfigFile | undefined, + root: string | undefined, +): Promise { if (configFile) { - const config = (await cspell.loadConfig(configFile)) || {}; - return { source: configFile, config }; + const cfgFile = typeof configFile === 'string' ? await readConfigHandleError(configFile) : configFile; + const config = await cspell.resolveConfigFileImports(cfgFile); + const source = toFilePathOrHref(cfgFile.url); + return { source, config }; } const config = await cspell.searchForConfig(root); return { source: config?.__importRef?.filename || 'None found', config: config || {} }; } +export function readConfigFile(filename: string | URL): Promise { + return cspell.readConfigFile(filename); +} + +async function readConfigHandleError(filename: string | URL): Promise { + try { + return await readConfigFile(filename); + } catch (e) { + const settings: cspell.CSpellSettingsWithSourceTrace = { + __importRef: { + filename: filename.toString(), + error: e as Error, + }, + }; + return { url: filenameToUrl(filename), settings }; + } +} + export interface FileInfo { filename: string; text?: string; @@ -82,7 +106,8 @@ export function fileInfoToDocument( return fileToDocument(uri.href, text, languageId, locale); } -export function filenameToUrl(filename: string, cwd = '.'): URL { +export function filenameToUrl(filename: string | URL, cwd = '.'): URL { + if (filename instanceof URL) return filename; const cwdURL = toFileDirURL(cwd); if (filename === STDIN) return new URL('stdin:///'); if (isStdinUrl(filename)) {