diff --git a/README.md b/README.md index b38b377..3c7a053 100644 --- a/README.md +++ b/README.md @@ -193,3 +193,37 @@ Directories that do not have `./index.js` in themselves will be excluded. When run again, `create-index` will update existing `./index.js` if it starts with `// @create-index\n\n`. If `create-index` is executed against a directory that contains `./index.js`, which does not start with `// @create-index\n\n`, an error will be thrown. + +## Ignore files on `--update` + +`create-index` can ignore files in a directory if `./index.js` contains special object with defined `ignore` property which takes `an array` of `regular expressions` defined as `strings`, e.g. + +```js +> cat index.js +// @create-index {"ignore": ["/baz.js$/"]} +``` + +```js +> tree ./ +./ +├── bar.js +├── baz.js +├── foo.js +└── index.js + +0 directories, 4 files +``` + +Given the above directory contents, after running `create-index` with `--update` flag, `./index.js` will be: + +```js +// @create-index {"ignore": ["/baz.js$/"]} + +import { default as bar } from './bar.js'; +import { default as foo } from './foo.js'; + +export { + bar, + foo +}; +``` diff --git a/src/utilities/constants.js b/src/utilities/constants.js new file mode 100644 index 0000000..acdc67c --- /dev/null +++ b/src/utilities/constants.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line +export const CREATE_INDEX_PATTERN = /(?:^|[\n\r]+)\/\/ @create-index\s?({.*})?[\n\r]+/; diff --git a/src/utilities/createIndexCode.js b/src/utilities/createIndexCode.js index a39faa0..381a6f4 100644 --- a/src/utilities/createIndexCode.js +++ b/src/utilities/createIndexCode.js @@ -24,8 +24,10 @@ const buildExportBlock = (files) => { export default (filePaths, options = {}) => { let code; + let configCode; code = ''; + configCode = ''; if (options.banner) { const banners = _.isArray(options.banner) ? options.banner : [options.banner]; @@ -37,7 +39,11 @@ export default (filePaths, options = {}) => { code += '\n'; } - code += '// @create-index\n\n'; + if (options.config && _.size(options.config) > 0) { + configCode += ' ' + JSON.stringify(options.config); + } + + code += '// @create-index' + configCode + '\n\n'; if (filePaths.length) { const sortedFilePaths = filePaths.sort(); diff --git a/src/utilities/hasIndex.js b/src/utilities/hasIndex.js new file mode 100644 index 0000000..1146e4c --- /dev/null +++ b/src/utilities/hasIndex.js @@ -0,0 +1,14 @@ +import fs from 'fs'; +import path from 'path'; + +export default (directoryPath) => { + const indexPath = path.resolve(directoryPath, 'index.js'); + + try { + fs.statSync(indexPath); + + return true; + } catch (error) { + return false; + } +}; diff --git a/src/utilities/log.js b/src/utilities/log.js index 97b86d0..06b6601 100644 --- a/src/utilities/log.js +++ b/src/utilities/log.js @@ -2,8 +2,6 @@ import chalk from 'chalk'; import moment from 'moment'; export default (...append) => { - /* eslint-disable no-console */ + // eslint-disable-next-line console.log(chalk.dim('[' + moment().format('HH:mm:ss') + ']'), ...append); - - /* eslint-enable no-console */ }; diff --git a/src/utilities/readDirectory.js b/src/utilities/readDirectory.js index 8ce8da8..2c7550b 100644 --- a/src/utilities/readDirectory.js +++ b/src/utilities/readDirectory.js @@ -1,20 +1,9 @@ import fs from 'fs'; import path from 'path'; import _ from 'lodash'; +import hasIndex from './hasIndex'; import validateTargetDirectory from './validateTargetDirectory'; -const hasIndex = (directoryPath) => { - const indexPath = path.resolve(directoryPath, 'index.js'); - - try { - fs.statSync(indexPath); - - return true; - } catch (error) { - return false; - } -}; - const hasNoExtension = (fileName) => { const matches = fileName.match(/\./g); @@ -54,15 +43,47 @@ const removeDuplicates = (files, preferredExtension) => { }); }; -export default (directoryPath, options = {}) => { - let children; +const removeIgnoredFiles = (files, ignorePatterns = []) => { + if (ignorePatterns.length === 0) { + return files; + } + + const patterns = ignorePatterns.map((pattern) => { + if (_.startsWith(pattern, '/') && _.endsWith(pattern, '/')) { + const patternWithoutSlashes = pattern.slice(1, -1); + return new RegExp(patternWithoutSlashes); + } + + return new RegExp(pattern); + }); + + return _.filter(files, (fileName) => { + let pattern; + + for (pattern of patterns) { + if (fileName.match(pattern) !== null) { + return false; + } + } + + return true; + }); +}; + +export default (directoryPath, options = {}) => { if (!validateTargetDirectory(directoryPath, {silent: options.silent})) { return false; } + const { + extensions = ['js'], + config = {} + } = options; + + let children; + children = fs.readdirSync(directoryPath); - const {extensions = ['js']} = options; children = _.filter(children, (fileName) => { const absolutePath = path.resolve(directoryPath, fileName); @@ -98,6 +119,7 @@ export default (directoryPath, options = {}) => { }); children = removeDuplicates(children, extensions[0]); + children = removeIgnoredFiles(children, config.ignore); return children.sort(); }; diff --git a/src/utilities/readIndexConfig.js b/src/utilities/readIndexConfig.js new file mode 100644 index 0000000..5bb8a2a --- /dev/null +++ b/src/utilities/readIndexConfig.js @@ -0,0 +1,32 @@ +import fs from 'fs'; +import path from 'path'; +import hasIndex from './hasIndex'; +import {CREATE_INDEX_PATTERN} from './constants'; + +export default (directoryPath) => { + if (!hasIndex(directoryPath)) { + return {}; + } + + const indexPath = path.resolve(directoryPath, 'index.js'); + const indexContents = fs.readFileSync(indexPath, 'utf-8'); + const found = indexContents.match(CREATE_INDEX_PATTERN); + const configLine = typeof found[1] === 'string' ? found[1].trim() : ''; + + if (configLine.length === 0) { + return {}; + } + + let config; + + try { + config = JSON.parse(configLine); + } catch (error) { + throw new Error( + '"' + indexPath + '" contains invalid configuration object.\n' + + 'Configuration object must be a valid JSON.' + ); + } + + return config; +}; diff --git a/src/utilities/validateTargetDirectory.js b/src/utilities/validateTargetDirectory.js index c646b5a..99f457b 100644 --- a/src/utilities/validateTargetDirectory.js +++ b/src/utilities/validateTargetDirectory.js @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import {CREATE_INDEX_PATTERN} from './constants'; export default (targetDirectory, options = {}) => { const silent = options.silent; @@ -33,7 +34,7 @@ export default (targetDirectory, options = {}) => { const indexFile = fs.readFileSync(indexFilePath, 'utf8'); - if (!indexFile.match(/(?:^|[\n\r]+)\/\/ @create-index[\n\r]+/)) { + if (!indexFile.match(CREATE_INDEX_PATTERN)) { if (silent) { return false; } else { diff --git a/src/utilities/writeIndex.js b/src/utilities/writeIndex.js index bfbba01..eea57fc 100644 --- a/src/utilities/writeIndex.js +++ b/src/utilities/writeIndex.js @@ -4,6 +4,7 @@ import _ from 'lodash'; import createIndexCode from './createIndexCode'; import validateTargetDirectory from './validateTargetDirectory'; import readDirectory from './readDirectory'; +import readIndexConfig from './readIndexConfig'; import sortByDepth from './sortByDepth'; export default (directoryPaths, options = {}) => { @@ -13,8 +14,10 @@ export default (directoryPaths, options = {}) => { }); _.forEach(sortedDirectoryPaths, (directoryPath) => { - const siblings = readDirectory(directoryPath, options); - const indexCode = createIndexCode(siblings); + const config = readIndexConfig(directoryPath); + const optionsWithConfig = Object.assign({}, options, {config}); + const siblings = readDirectory(directoryPath, optionsWithConfig); + const indexCode = createIndexCode(siblings, {config}); const indexFilePath = path.resolve(directoryPath, 'index.js'); fs.writeFileSync(indexFilePath, indexCode); diff --git a/src/utilities/writeIndexCli.js b/src/utilities/writeIndexCli.js index f37a0fa..df0b513 100644 --- a/src/utilities/writeIndexCli.js +++ b/src/utilities/writeIndexCli.js @@ -5,6 +5,7 @@ import chalk from 'chalk'; import createIndexCode from './createIndexCode'; import validateTargetDirectory from './validateTargetDirectory'; import readDirectory from './readDirectory'; +import readIndexConfig from './readIndexConfig'; import sortByDepth from './sortByDepth'; import log from './log'; import findIndexFiles from './findIndexFiles'; @@ -44,13 +45,17 @@ export default (directoryPaths, options = {}) => { _.forEach(sortedDirectoryPaths, (directoryPath) => { let existingIndexCode; + const config = readIndexConfig(directoryPath); + const siblings = readDirectory(directoryPath, { + config, extensions: options.extensions, silent: options.ignoreUnsafe }); const indexCode = createIndexCode(siblings, { - banner: options.banner + banner: options.banner, + config }); const indexFilePath = path.resolve(directoryPath, 'index.js'); diff --git a/test/createIndexCode.js b/test/createIndexCode.js index 5feb6c6..d9b259b 100644 --- a/test/createIndexCode.js +++ b/test/createIndexCode.js @@ -12,7 +12,7 @@ describe('createIndexCode()', () => { expect(indexCode).to.equal(codeExample(` // @create-index - `)); + `)); }); it('describes a single child', () => { const indexCode = createIndexCode(['foo']); @@ -21,7 +21,7 @@ describe('createIndexCode()', () => { // @create-index export { default as foo } from './foo'; - `)); + `)); }); it('describes multiple children', () => { const indexCode = createIndexCode(['bar', 'foo']); @@ -31,7 +31,7 @@ export { default as foo } from './foo'; export { default as bar } from './bar'; export { default as foo } from './foo'; - `)); + `)); }); context('file with extension', () => { it('removes the extension from the export statement', () => { @@ -41,7 +41,7 @@ export { default as foo } from './foo'; // @create-index export { default as foo } from './foo.js'; - `)); + `)); }); }); context('multiple, unsorted', () => { @@ -53,7 +53,23 @@ export { default as foo } from './foo.js'; export { default as bar } from './bar'; export { default as foo } from './foo'; - `)); + `)); + }); + }); + + context('with config', () => { + it('should append config', () => { + const config = { + ignore: ['/^zoo/'] + }; + const indexCode = createIndexCode(['foo', 'bar'], {config}); + + expect(indexCode).to.equal(codeExample(` +// @create-index {"ignore":["/^zoo/"]} + +export { default as bar } from './bar'; +export { default as foo } from './foo'; + `)); }); }); }); diff --git a/test/fixtures/read-index-config/no-index/foo.js b/test/fixtures/read-index-config/no-index/foo.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/read-index-config/with-config/index.js b/test/fixtures/read-index-config/with-config/index.js new file mode 100644 index 0000000..9d9bb61 --- /dev/null +++ b/test/fixtures/read-index-config/with-config/index.js @@ -0,0 +1 @@ +// @create-index {"ignore": ["/foo.js$/"]} diff --git a/test/fixtures/read-index-config/with-invalid-config/index.js b/test/fixtures/read-index-config/with-invalid-config/index.js new file mode 100644 index 0000000..d4a8f1d --- /dev/null +++ b/test/fixtures/read-index-config/with-invalid-config/index.js @@ -0,0 +1 @@ +// @create-index {ignore: 'foo'} diff --git a/test/fixtures/read-index-config/without-config/index.js b/test/fixtures/read-index-config/without-config/index.js new file mode 100644 index 0000000..b83b7f9 --- /dev/null +++ b/test/fixtures/read-index-config/without-config/index.js @@ -0,0 +1 @@ +// @create-index diff --git a/test/fixtures/write-index/with-config/bar.js b/test/fixtures/write-index/with-config/bar.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/write-index/with-config/foo.js b/test/fixtures/write-index/with-config/foo.js new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/write-index/with-config/index.js b/test/fixtures/write-index/with-config/index.js new file mode 100644 index 0000000..f31701e --- /dev/null +++ b/test/fixtures/write-index/with-config/index.js @@ -0,0 +1,4 @@ +// @create-index {"ignore":["/bar.js$/"]} + +export { default as foo } from './foo.js'; + diff --git a/test/readIndexConfig.js b/test/readIndexConfig.js new file mode 100644 index 0000000..8592db5 --- /dev/null +++ b/test/readIndexConfig.js @@ -0,0 +1,56 @@ +import path from 'path'; +import { + expect +} from 'chai'; +import readIndexConfig from '../src/utilities/readIndexConfig'; + +const fixtures = { + noIndex: path.resolve(__dirname, 'fixtures/read-index-config/no-index'), + withConfig: path.resolve(__dirname, 'fixtures/read-index-config/with-config'), + withInvalidConfig: path.resolve(__dirname, 'fixtures/read-index-config/with-invalid-config'), + withoutConfig: path.resolve(__dirname, 'fixtures/read-index-config/without-config') +}; + +const expectedValues = { + noIndex: {}, + withConfig: { + ignore: ['/foo.js$/'] + }, + withoutConfig: {} +}; + +describe('readIndexConfig()', () => { + context('When valid config is defined', () => { + it('reads config object', () => { + const config = readIndexConfig(fixtures.withConfig); + + expect(config).to.deep.equal(expectedValues.withConfig); + }); + }); + + context('When invalid config is defined', () => { + it('should throw an error', () => { + const wrappedReadIndexConfig = () => { + readIndexConfig(fixtures.withInvalidConfig); + }; + + expect(wrappedReadIndexConfig).to.throw(/Configuration object must be a valid JSON./); + }); + }); + + context('When config is NOT defined', () => { + it('returns an empty object', () => { + const config = readIndexConfig(fixtures.withoutConfig); + + expect(config).to.deep.equal(expectedValues.withoutConfig); + }); + }); + + context('When index file doesn\'t exist', () => { + it('returns an empty object', () => { + const config = readIndexConfig(fixtures.withoutConfig); + + expect(config).to.deep.equal(expectedValues.noIndex); + }); + }); +}); diff --git a/test/writeIndex.js b/test/writeIndex.js index 828969b..be947bb 100644 --- a/test/writeIndex.js +++ b/test/writeIndex.js @@ -8,29 +8,51 @@ import { import writeIndex from '../src/utilities/writeIndex'; import codeExample from './codeExample'; +const readFile = (filePath) => { + return fs.readFileSync(filePath, 'utf8'); +}; + +const removeFile = (filePath) => { + fs.unlinkSync(filePath); +}; + +const appendToFile = (filePath, content) => { + fs.appendFileSync(filePath, content, 'utf-8'); +}; + const fixturesPath = path.resolve(__dirname, 'fixtures/write-index'); describe('writeIndex()', () => { it('creates index in target directory', () => { const indexFilePath = path.resolve(fixturesPath, 'mixed/index.js'); - try { - fs.unlinkSync(indexFilePath); + removeFile(indexFilePath); + writeIndex([path.resolve(fixturesPath, 'mixed')]); + const indexCode = readFile(indexFilePath); - // eslint-disable-next-line no-empty - } catch (error) { + expect(indexCode).to.equal(codeExample(` +// @create-index - } +export { default as bar } from './bar'; +export { default as foo } from './foo.js'; + `)); + }); - writeIndex([path.resolve(fixturesPath, 'mixed')]); + it('creates index with config in target directory', () => { + const indexFilePath = path.resolve(fixturesPath, 'with-config/index.js'); + // eslint-disable-next-line + const ignoredExportLine = `export { default as bar } from './bar.js';`; - const indexCode = fs.readFileSync(indexFilePath, 'utf8'); + appendToFile(indexFilePath, ignoredExportLine); + expect(readFile(indexFilePath).includes(ignoredExportLine)).to.equal(true); + + writeIndex([path.resolve(fixturesPath, 'with-config')]); + const indexCode = readFile(indexFilePath); expect(indexCode).to.equal(codeExample(` -// @create-index +// @create-index {"ignore":["/bar.js$/"]} -export { default as bar } from './bar'; export { default as foo } from './foo.js'; - `)); + `)); }); });