diff --git a/packages/cspell/src/__snapshots__/app.test.ts.snap b/packages/cspell/src/__snapshots__/app.test.ts.snap index 2d17607fbf5..60111d5b8e8 100644 --- a/packages/cspell/src/__snapshots__/app.test.ts.snap +++ b/packages/cspell/src/__snapshots__/app.test.ts.snap @@ -10551,8 +10551,8 @@ exports[`Validate cli > app help [ 'init', '--help' ] 1`] = ` " --dictionary Enable a dictionary. Can be used multiple times.", " --no-comments Do not add comments to the config file.", " --remove-comments Remove all comments from the config file.", - " --schema Add or update the schema reference. This is the", - " default for a new file.", + " --schema Add or update the schema reference. Default for a", + " config with no settings.", " --no-schema Do not add the schema reference to the config file.", " --stdout Write the configuration to stdout instead of a", " file.", diff --git a/packages/cspell/src/commandInit.test.ts b/packages/cspell/src/commandInit.test.ts index ceb6e72cfb9..c32fc560a9a 100644 --- a/packages/cspell/src/commandInit.test.ts +++ b/packages/cspell/src/commandInit.test.ts @@ -11,7 +11,7 @@ import { run } from './app.mjs'; const oldSchema = 'https://example.com/old.schema.json'; const yamlSchemaComment = `# yaml-language-server: $schema=${cspellConfigFileSchema}`; -describe('cspell init $schema', () => { +describe('cspell init', () => { let tempDir: string; beforeEach(async () => { @@ -84,4 +84,60 @@ describe('cspell init $schema', () => { const content = await init('cspell.config.yaml', ...args); expect(content.includes(yamlSchemaComment)).toBe(expected); }); + + async function initConfig(fileName: string, ...args: string[]): Promise { + const filePath = path.join(tempDir, fileName); + await run(new Command(), [...process.argv.slice(0, 2), 'init', '--config', filePath, ...args]); + return fs.readFile(filePath, 'utf8'); + } + + const newFileSettings = { $schema: cspellConfigFileSchema, version: '0.2', language: 'en' }; + + test.each` + fileName + ${'cspell.json'} + ${'cspell.jsonc'} + `('new $fileName with --config', async ({ fileName }) => { + expect(JSON.parse(await initConfig(fileName, '--no-comments'))).toEqual(newFileSettings); + }); + + test('new JSON file with --output and the default format', async () => { + const filePath = path.join(tempDir, 'cspell.json'); + await run(new Command(), [...process.argv.slice(0, 2), 'init', '--output', filePath]); + expect(JSON.parse(await fs.readFile(filePath, 'utf8'))).toEqual(newFileSettings); + }); + + test('existing JSON file only changes what was asked for', async () => { + await writeConfig('cspell.json', '{ "words": ["one"] }\n'); + expect(JSON.parse(await initConfig('cspell.json'))).toEqual({ words: ['one'] }); + expect(JSON.parse(await initConfig('cspell.json', '--locale', 'en-GB'))).toEqual({ + words: ['one'], + language: 'en-GB', + }); + }); + + test('existing YAML file is unchanged', async () => { + const content = 'words:\n - one\n'; + await writeConfig('cspell.config.yaml', content); + expect(await initConfig('cspell.config.yaml')).toBe(content); + }); + + test('existing file with no settings is initialized', async () => { + await writeConfig('cspell.json', '{}\n'); + expect(JSON.parse(await initConfig('cspell.json'))).toEqual(newFileSettings); + }); + + test('package.json without a cspell section gets one', async () => { + await writeConfig('package.json', '{ "name": "sample" }\n'); + expect(JSON.parse(await initConfig('package.json'))).toEqual({ + name: 'sample', + cspell: { version: '0.2', language: 'en' }, + }); + }); + + test('package.json with a cspell section is unchanged', async () => { + const packageJson = { name: 'sample', cspell: { words: ['one'] } }; + await writeConfig('package.json', JSON.stringify(packageJson)); + expect(JSON.parse(await initConfig('package.json'))).toEqual(packageJson); + }); }); diff --git a/packages/cspell/src/commandInit.ts b/packages/cspell/src/commandInit.ts index 68659d504ef..3fd51508cbb 100644 --- a/packages/cspell/src/commandInit.ts +++ b/packages/cspell/src/commandInit.ts @@ -31,7 +31,7 @@ export function commandInit(prog: Command): Command { .option('--no-comments', 'Do not add comments to the config file.') .addOption(crOpt('--remove-comments', 'Remove all comments from the config file.').implies({ comments: false })) .addOption( - crOpt('--schema', 'Add or update the schema reference. This is the default for a new file.').default( + crOpt('--schema', 'Add or update the schema reference. Default for a config with no settings.').default( undefined, ), ) diff --git a/packages/cspell/src/config/adjustConfig.ts b/packages/cspell/src/config/adjustConfig.ts index eb3aa953f1f..3fef5847f16 100644 --- a/packages/cspell/src/config/adjustConfig.ts +++ b/packages/cspell/src/config/adjustConfig.ts @@ -10,7 +10,7 @@ import { isCfgArrayNode, MutableCSpellConfigFile } from 'cspell-config-lib'; import { toError } from '../util/errors.js'; -export async function fileExists(url: URL): Promise { +async function fileExists(url: URL): Promise { if (url.protocol !== 'file:') { return false; // Only check file URLs } diff --git a/packages/cspell/src/config/configInit.ts b/packages/cspell/src/config/configInit.ts index b1956f43ab1..0c254895875 100644 --- a/packages/cspell/src/config/configInit.ts +++ b/packages/cspell/src/config/configInit.ts @@ -6,8 +6,9 @@ import type { CSpellConfigFile, CSpellConfigFileReaderWriter } from 'cspell-conf import { createReaderWriter, cspellConfigFileSchema } from 'cspell-config-lib'; import { console } from '../console.js'; -import { addDictionariesToConfigFile, addImportsToConfigFile, fileExists } from './adjustConfig.js'; +import { addDictionariesToConfigFile, addImportsToConfigFile } from './adjustConfig.js'; import { applyValuesToConfigFile } from './config.js'; +import type { CommentConfig } from './constants.js'; import { defaultConfig } from './constants.js'; import type { InitOptions } from './options.js'; @@ -24,9 +25,8 @@ const defaultConfigYaml = ` export async function configInit(options: InitOptions): Promise { const rw = createReaderWriter(); const url = determineFileNameURL(options); - const schema = options.schema ?? !(await fileExists(url)); - const configFile = await createConfigFile(rw, url, options); - await applyOptionsToConfigFile(configFile, { ...options, schema }); + const configFile = await createConfigFile(rw, url); + await applyOptionsToConfigFile(configFile, options); await fs.mkdir(new URL('./', configFile.url), { recursive: true }); if (options.stdout) { @@ -38,6 +38,7 @@ export async function configInit(options: InitOptions): Promise { async function applyOptionsToConfigFile(configFile: CSpellConfigFile, options: InitOptions): Promise { const settings: CSpellSettings = {}; + const hasSettings = Object.keys(configFile.settings).length > 0; const addComments = options.comments || @@ -47,7 +48,7 @@ async function applyOptionsToConfigFile(configFile: CSpellConfigFile, options: I configFile.removeAllComments(); } - if (options.schema) { + if (options.schema ?? !hasSettings) { configFile.setSchema(schemaRef); } @@ -55,7 +56,8 @@ async function applyOptionsToConfigFile(configFile: CSpellConfigFile, options: I settings.language = options.locale; } - applyValuesToConfigFile(configFile, settings, defaultConfig, addComments); + const defaults = hasSettings ? withoutDefaultValues(defaultConfig) : defaultConfig; + applyValuesToConfigFile(configFile, settings, defaults, addComments); if (options.import) { await addImportsToConfigFile( @@ -76,6 +78,10 @@ async function applyOptionsToConfigFile(configFile: CSpellConfigFile, options: I return configFile; } +function withoutDefaultValues(config: CommentConfig): CommentConfig { + return Object.fromEntries(Object.entries(config).map(([key, entry]) => [key, { ...entry, value: undefined }])); +} + function determineFileNameURL(options: InitOptions): URL { if (options.config) { return toFileURL(options.config); @@ -110,32 +116,16 @@ function determineDefaultFileName(options: InitOptions): string { throw new Error(`Unsupported format: ${options.format}`); } -function getDefaultContent(options: InitOptions): string { - switch (options.format) { - case undefined: - case 'yaml': { - return defaultConfigYaml; - } - case 'json': - case 'jsonc': { - return defaultConfigJson; - } - default: { - throw new Error(`Unsupported format: ${options.format}`); - } - } +function getDefaultContent(url: URL): string { + return /\.jsonc?$/.test(url.pathname) ? defaultConfigJson : defaultConfigYaml; } -async function createConfigFile( - rw: CSpellConfigFileReaderWriter, - url: URL, - options: InitOptions, -): Promise { +async function createConfigFile(rw: CSpellConfigFileReaderWriter, url: URL): Promise { if (url.pathname.endsWith('package.json')) { return rw.readConfig(url); } - const content = await fs.readFile(url, 'utf8').catch(() => getDefaultContent(options)); + const content = await fs.readFile(url, 'utf8').catch(() => getDefaultContent(url)); return rw.parse({ url, content }); } diff --git a/packages/cspell/src/config/options.ts b/packages/cspell/src/config/options.ts index df05e714da9..cddfc84ec3c 100644 --- a/packages/cspell/src/config/options.ts +++ b/packages/cspell/src/config/options.ts @@ -33,7 +33,7 @@ export interface BaseConfigOptions { * Whether to add the schema reference to the configuration file. * - `true` - the schema reference will be added / updated. * - `false` - no schema reference will be added. - * - `undefined` - the schema reference will be added only when creating a new file. + * - `undefined` - the schema reference will be added only if the configuration has no settings yet. */ schema?: boolean; /**