Skip to content

Commit

Permalink
feat: Support calling a function to get the config (#5126)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Dec 29, 2023
1 parent 6293413 commit 6d083a3
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 1 deletion.
65 changes: 65 additions & 0 deletions packages/cspell-config-lib/fixtures/js/module/cspell.custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { promises as fs } from 'fs';

function expand(pattern, options = { begin: '(', end: ')', sep: '|' }, start = 0) {
const len = pattern.length;
const parts = [];
function push(word) {
if (Array.isArray(word)) {
parts.push(...word);
} else {
parts.push(word);
}
}
let i = start;
let curWord = '';
while (i < len) {
const ch = pattern[i++];
if (ch === options.end) {
break;
}
if (ch === options.begin) {
const nested = expand(pattern, options, i);
i = nested.idx;
curWord = nested.parts.flatMap((p) => (Array.isArray(curWord) ? curWord.map((w) => w + p) : [curWord + p]));
continue;
}
if (ch === options.sep) {
push(curWord);
curWord = '';
continue;
}
curWord = Array.isArray(curWord) ? curWord.map((w) => w + ch) : curWord + ch;
}
push(curWord);
return { parts, idx: i };
}

function expandWord(pattern, options = { begin: '(', end: ')', sep: '|' }) {
return expand(pattern, options).parts;
}

function expandWords(wordList) {
const words = wordList
.split(/\n/g)
.map((w) => w.trim())
.filter((w) => !!w && !w.startsWith('#'))
.flatMap((w) => expandWord(w));
return words;
}

const wordsFile = new URL('words.txt', import.meta.url);

const wordList = await fs.readFile(wordsFile, 'utf8');

const config = {
id: 'async-module',
dictionaryDefinitions: [
{
name: 'custom-words',
words: expandWords(wordList),
},
],
dictionaries: ['custom-words'],
};

export default config;
63 changes: 63 additions & 0 deletions packages/cspell-config-lib/fixtures/js/module/cspell.function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { promises as fs } from 'fs';

function expand(pattern, options = { begin: '(', end: ')', sep: '|' }, start = 0) {
const len = pattern.length;
const parts = [];
function push(word) {
if (Array.isArray(word)) {
parts.push(...word);
} else {
parts.push(word);
}
}
let i = start;
let curWord = '';
while (i < len) {
const ch = pattern[i++];
if (ch === options.end) {
break;
}
if (ch === options.begin) {
const nested = expand(pattern, options, i);
i = nested.idx;
curWord = nested.parts.flatMap((p) => (Array.isArray(curWord) ? curWord.map((w) => w + p) : [curWord + p]));
continue;
}
if (ch === options.sep) {
push(curWord);
curWord = '';
continue;
}
curWord = Array.isArray(curWord) ? curWord.map((w) => w + ch) : curWord + ch;
}
push(curWord);
return { parts, idx: i };
}

function expandWord(pattern, options = { begin: '(', end: ')', sep: '|' }) {
return expand(pattern, options).parts;
}

function expandWords(wordList) {
const words = wordList
.split(/\n/g)
.map((w) => w.trim())
.filter((w) => !!w && !w.startsWith('#'))
.flatMap((w) => expandWord(w));
return words;
}

/**
* @returns {Promise<import('@cspell/cspell-types').CSpellUserSettings>}
*/
export default async function getConfig() {
const wordsFile = new URL('words.txt', import.meta.url);

const wordList = await fs.readFile(wordsFile, 'utf8');

const config = {
id: 'config-function',
words: expandWords(wordList),
};
return config;
}
11 changes: 11 additions & 0 deletions packages/cspell-config-lib/fixtures/js/module/cspell.python.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { readFile } from 'node:fs/promises';

/**
* @returns {Promise<import('@cspell/cspell-types').CSpellUserSettings>}
*/
export default async function getConfig() {
const words = (await readFile(new URL('requirements.txt', import.meta.url), 'utf8'))
.replace(/[.,]|([=<>].*)/g, ' ')
.split(/\s+/g);
return { id: 'python-imports', words };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
blinker==1.4
click==7.1.2
lazr.restfulclient==0.14.2
lazr.uri==1.0.5
3 changes: 3 additions & 0 deletions packages/cspell-config-lib/fixtures/js/module/words.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# words
(un|re|)check(s|ed|ing|)
(un|re|)test(s|ed|ing|)
37 changes: 37 additions & 0 deletions packages/cspell-config-lib/src/loaders/loaderJavaScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fixtures } from '../test-helpers/fixtures.js';
import { loaderJavaScript } from './loaderJavaScript.js';

const oc = expect.objectContaining;
const ac = expect.arrayContaining;

describe('loaderJavaScript', () => {
afterEach(() => {});
Expand All @@ -17,6 +18,7 @@ describe('loaderJavaScript', () => {
${'js/module/cspell.config.cjs'} | ${{ settings: oc({ id: 'module/cjs' }) }}
${'js/commonjs/cspell.config.js'} | ${{ settings: oc({ id: 'commonjs/js' }) }}
${'js/commonjs/cspell.config.mjs'} | ${{ settings: oc({ id: 'commonjs/mjs' }) }}
${'js/module/cspell.custom.js'} | ${{ settings: oc({ id: 'async-module', dictionaryDefinitions: [oc({ words: ac(['recheck', 'tested']) })] }) }}
`('loaderJavaScript $file', async ({ file, expected }) => {
const url = pathToFileURL(fixtures(file));
expected.url ??= url;
Expand Down Expand Up @@ -44,6 +46,41 @@ describe('loaderJavaScript', () => {
expect(result4.settings).not.toBe(result.settings);
});

/* cspell:ignore lazr */

test.each`
file | expected
${'js/module/cspell.function.js'} | ${{ settings: oc({ id: 'config-function', words: ac(['recheck', 'tested']) }) }}
${'js/module/cspell.python.mjs'} | ${{ settings: oc({ id: 'python-imports', words: ac(['blinker', 'click', 'lazr']) }) }}
`('loaderJavaScript $file default function', async ({ file, expected }) => {
const url = pathToFileURL(fixtures(file));
expected.url ??= url;
const next = vi.fn();

const result = await loaderJavaScript.load({ url, context: { deserialize, io: defaultIO } }, next);
expect(result).toEqual(expected);

// Try double loading.
const result2 = await loaderJavaScript.load({ url, context: { deserialize, io: defaultIO } }, next);
expect(result2.settings).toEqual(result.settings);
// These are not the same because it is a function result, not a static object.
expect(result2.settings).not.toBe(result.settings);

// Ensure that we can force a load by changing search params.
const url3 = new URL(url.href);
url3.searchParams.append('q', '29');

const result3 = await loaderJavaScript.load({ url: url3, context: { deserialize, io: defaultIO } }, next);
expect(result3.settings).not.toBe(result.settings);
expect(result3.settings).toEqual(result.settings);

// Ensure that we can force a load by changing the hash.
const url4 = new URL(url.href);
url4.hash = 'hash';
const result4 = await loaderJavaScript.load({ url: url4, context: { deserialize, io: defaultIO } }, next);
expect(result4.settings).not.toBe(result.settings);
});

test.each`
file | expected
${'js/commonjs/cspell.config.mjs'} | ${{ settings: oc({ id: 'commonjs/mjs' }) }}
Expand Down
3 changes: 2 additions & 1 deletion packages/cspell-config-lib/src/loaders/loaderJavaScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ async function importJavaScript(url: URL, hashSuffix: number | string): Promise<
_url.hash = `${_url.hash};loaderSuffix=${hashSuffix}`;
_log('importJavaScript: %o', { url: _url.href });
const result = await import(_url.href);
const settings = result.default ?? result;
const settingsOrFunction = await (result.default ?? result);
const settings = typeof settingsOrFunction === 'function' ? await settingsOrFunction() : settingsOrFunction;
return new CSpellConfigFileJavaScript(url, settings);
} catch (e) {
_log('importJavaScript Error: %o', { url: url.href, error: e, hashSuffix });
Expand Down
34 changes: 34 additions & 0 deletions packages/cspell-dictionary/src/util/braceExpansion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from 'vitest';

import { expandBraces } from './braceExpansion.js';

describe('Validate braceExpansion', () => {
test('expandBraces should return an array of expanded strings', () => {
// Test case 1
const result1 = expandBraces('a{b,c}d', { begin: '{', end: '}', sep: ',' });
expect(result1).toEqual(['abd', 'acd']);

// Test case 2
const result2 = expandBraces('z{a,b,c}', { begin: '{', end: '}', sep: ',' });
expect(result2).toEqual(['za', 'zb', 'zc']);
});

/* cspell:ignore unchecker */

test.each`
text | expected
${'hello'} | ${s('hello')}
${'remember(s|ed|ing|er|)'} | ${s('remembers|remembered|remembering|rememberer|remember')}
${'remember(s|e(d|r)|ing|)'} | ${s('remembers|remembered|rememberer|remembering|remember')}
${'remember(s|e(d|r)|ing|'} | ${s('remembers|remembered|rememberer|remembering|remember')}
${'(un|)check(s|e(d|r)|ing|)'} | ${s('unchecks|checks|unchecked|checked|unchecker|checker|unchecking|checking|uncheck|check')}
${'(un|check(s|e(d|r)|ing|)'} | ${s('un|checks|checked|checker|checking|check')}
${'(un|re|)check(ed|)'} | ${s('unchecked|rechecked|checked|uncheck|recheck|check')}
`('expandBraces $text', ({ text, expected }) => {
expect(expandBraces(text)).toEqual(expected);
});
});

function s(text: string, split = '|'): string[] {
return text.split(split);
}
47 changes: 47 additions & 0 deletions packages/cspell-dictionary/src/util/braceExpansion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export interface Options {
begin: string;
end: string;
sep: string;
}

function expand(
pattern: string,
options: Options = { begin: '(', end: ')', sep: '|' },
start = 0,
): { parts: string[]; idx: number } {
const len = pattern.length;
const parts: string[] = [];
function push(word: string | string[]) {
if (Array.isArray(word)) {
parts.push(...word);
} else {
parts.push(word);
}
}
let i = start;
let curWord: string | string[] = '';
while (i < len) {
const ch = pattern[i++];
if (ch === options.end) {
break;
}
if (ch === options.begin) {
const nested = expand(pattern, options, i);
i = nested.idx;
curWord = nested.parts.flatMap((p) => (Array.isArray(curWord) ? curWord.map((w) => w + p) : [curWord + p]));
continue;
}
if (ch === options.sep) {
push(curWord);
curWord = '';
continue;
}
curWord = Array.isArray(curWord) ? curWord.map((w) => w + ch) : curWord + ch;
}
push(curWord);
return { parts, idx: i };
}

export function expandBraces(pattern: string, options: Options = { begin: '(', end: ')', sep: '|' }): string[] {
return expand(pattern, options).parts;
}

0 comments on commit 6d083a3

Please sign in to comment.