diff --git a/packages/jsdoc-core/.gitignore b/packages/jsdoc-core/.gitignore new file mode 100644 index 00000000..b191b3b6 --- /dev/null +++ b/packages/jsdoc-core/.gitignore @@ -0,0 +1,2 @@ +# Dotfile detritus +.DS_Store diff --git a/packages/jsdoc-core/.npmignore b/packages/jsdoc-core/.npmignore new file mode 100644 index 00000000..c828232d --- /dev/null +++ b/packages/jsdoc-core/.npmignore @@ -0,0 +1,14 @@ +.editorconfig +.eslintignore +.eslintrc.js +.gitignore +.github/ +.renovaterc.json +.travis.yml +CHANGES.md +CODE_OF_CONDUCT.md +CONTRIBUTING.md +gulpfile.js +lerna.json +packages/ +test/ diff --git a/packages/jsdoc-core/README.md b/packages/jsdoc-core/README.md new file mode 100644 index 00000000..0d7bb8af --- /dev/null +++ b/packages/jsdoc-core/README.md @@ -0,0 +1,3 @@ +# `@jsdoc/core` + +Core functionality for JSDoc. diff --git a/packages/jsdoc-core/index.js b/packages/jsdoc-core/index.js new file mode 100644 index 00000000..b85eb833 --- /dev/null +++ b/packages/jsdoc-core/index.js @@ -0,0 +1,11 @@ +/** + * Provides core functionality for JSDoc. + * + * @module @jsdoc/config + */ + +const config = require('./lib/config'); + +module.exports = { + config +}; diff --git a/packages/jsdoc-core/lib/config.js b/packages/jsdoc-core/lib/config.js new file mode 100644 index 00000000..35d1a4ef --- /dev/null +++ b/packages/jsdoc-core/lib/config.js @@ -0,0 +1,134 @@ +/** + * Manages configuration settings for JSDoc. + * + * @alias module:@jsdoc/core.config + */ + +const _ = require('lodash'); +const cosmiconfig = require('cosmiconfig'); +const stripBom = require('strip-bom'); +const stripJsonComments = require('strip-json-comments'); + +const MODULE_NAME = 'jsdoc'; + +const defaults = exports.defaults = { + // TODO(hegemonic): Integrate CLI options with other options. + opts: { + destination: './out', + encoding: 'utf8' + }, + /** + * The JSDoc plugins to load. + */ + plugins: [], + // TODO(hegemonic): Move to `source` or remove. + recurseDepth: 10, + /** + * Settings for loading and parsing source files. + */ + source: { + /** + * A regular expression that matches source files to exclude from processing. + * + * To exclude files if any portion of their path begins with an underscore, use the value + * `(^|\\/|\\\\)_`. + */ + excludePattern: '', + /** + * A regular expression that matches source files that JSDoc should process. + * + * By default, all source files with the extensions `.js`, `.jsdoc`, and `.jsx` are + * processed. + */ + includePattern: '.+\\.js(doc|x)?$', + /** + * The type of source file. In general, you should use the value `module`. If none of your + * source files use ECMAScript >=2015 syntax, you can use the value `script`. + */ + type: 'module' + }, + /** + * Settings for interpreting JSDoc tags. + */ + tags: { + /** + * Set to `true` to allow tags that JSDoc does not recognize. + */ + allowUnknownTags: true, + // TODO(hegemonic): Use module paths, not magic strings. + /** + * The JSDoc tag dictionaries to load. + * + * If you specify two or more tag dictionaries, and a tag is defined in multiple + * dictionaries, JSDoc uses the definition from the first dictionary that includes that tag. + */ + dictionaries: [ + 'jsdoc', + 'closure' + ] + }, + /** + * Settings for generating output with JSDoc templates. Some JSDoc templates might ignore these + * settings. + */ + templates: { + /** + * Set to `true` to use a monospaced font for links to other code symbols, but not links to + * websites. + */ + cleverLinks: false, + /** + * Set to `true` to use a monospaced font for all links. + */ + monospaceLinks: false + } +}; + +class Config { + constructor(filepath, config) { + this.config = config; + this.filepath = filepath; + } +} + +function loadJson(filepath, content) { + return cosmiconfig.loadJson(filepath, stripBom(stripJsonComments(content))); +} + +function loadYaml(filepath, content) { + return cosmiconfig.loadYaml(filepath, stripBom(content)); +} + +const explorer = cosmiconfig(MODULE_NAME, { + cache: false, + loaders: { + '.json': loadJson, + '.yaml': loadYaml, + '.yml': loadYaml, + noExt: loadYaml + }, + searchPlaces: [ + 'package.json', + `.${MODULE_NAME}rc`, + `.${MODULE_NAME}rc.json`, + `.${MODULE_NAME}rc.yaml`, + `.${MODULE_NAME}rc.yml`, + `.${MODULE_NAME}rc.js`, + `${MODULE_NAME}.config.js` + ] +}); + +exports.loadSync = (filepath) => { + let loaded; + + if (filepath) { + loaded = explorer.loadSync(filepath); + } else { + loaded = explorer.searchSync() || {}; + } + + return new Config( + loaded.filepath, + _.defaultsDeep({}, loaded.config, defaults) + ); +}; diff --git a/packages/jsdoc-core/package-lock.json b/packages/jsdoc-core/package-lock.json new file mode 100644 index 00000000..e62f6451 --- /dev/null +++ b/packages/jsdoc-core/package-lock.json @@ -0,0 +1,134 @@ +{ + "name": "@jsdoc/core", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "mock-fs": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.1.tgz", + "integrity": "sha512-w22rOL5ZYu6HbUehB5deurghGM0hS/xBVyHMGKOuQctkk93J9z9VEOhDsiWrXOprVNQpP9uzGKdl8v9mFspKuw==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==" + } + } +} diff --git a/packages/jsdoc-core/package.json b/packages/jsdoc-core/package.json new file mode 100644 index 00000000..bb9f1877 --- /dev/null +++ b/packages/jsdoc-core/package.json @@ -0,0 +1,30 @@ +{ + "name": "@jsdoc/core", + "version": "0.1.0", + "description": "Core functionality for JSDoc.", + "keywords": [ + "jsdoc" + ], + "author": "Jeff Williams ", + "homepage": "https://jsdoc.app/", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/jsdoc/jsdoc.git" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "bugs": { + "url": "https://github.com/jsdoc/jsdoc/issues" + }, + "dependencies": { + "cosmiconfig": "^5.2.1", + "lodash": "^4.17.15", + "strip-bom": "^4.0.0", + "strip-json-comments": "^3.0.1" + }, + "devDependencies": { + "mock-fs": "^4.10.1" + } +} diff --git a/packages/jsdoc-core/test/specs/index.js b/packages/jsdoc-core/test/specs/index.js new file mode 100644 index 00000000..287ed0ca --- /dev/null +++ b/packages/jsdoc-core/test/specs/index.js @@ -0,0 +1,14 @@ +const config = require('../../lib/config'); +const core = require('../../index'); + +describe('@jsdoc/core', () => { + it('exists', () => { + expect(core).toBeObject(); + }); + + describe('config', () => { + it('is lib/config', () => { + expect(core.config).toBe(config); + }); + }); +}); diff --git a/packages/jsdoc-core/test/specs/lib/config.js b/packages/jsdoc-core/test/specs/lib/config.js new file mode 100644 index 00000000..9ae2e05a --- /dev/null +++ b/packages/jsdoc-core/test/specs/lib/config.js @@ -0,0 +1,186 @@ +const { config } = require('@jsdoc/core'); +const mockFs = require('mock-fs'); + +describe('@jsdoc/config', () => { + afterEach(() => mockFs.restore()); + + it('exists', () => { + expect(config).toBeObject(); + }); + + describe('loadSync', () => { + it('exists', () => { + expect(config.loadSync).toBeFunction(); + }); + + it('returns an object with `config` and `filepath` properties', () => { + mockFs({ + 'conf.json': '{}' + }); + + const conf = config.loadSync('conf.json'); + + expect(conf.config).toBeObject(); + expect(conf.filepath).toEndWith('conf.json'); + }); + + it('loads settings from the specified filepath if there is one', () => { + mockFs({ + 'conf.json': '{"foo":"bar"}' + }); + + const conf = config.loadSync('conf.json'); + + expect(conf.config.foo).toBe('bar'); + }); + + it('finds the config file when no filepath is specified', () => { + mockFs({ + 'package.json': '{"jsdoc":{"foo":"bar"}}' + }); + + const conf = config.loadSync(); + + expect(conf.config.foo).toBe('bar'); + }); + + it('parses JSON config files that have an extension and contain comments', () => { + mockFs({ + '.jsdocrc.json': '// comment\n{"foo":"bar"}' + }); + + const conf = config.loadSync(); + + expect(conf.config.foo).toBe('bar'); + }); + + it('parses JSON files that start with a BOM', () => { + mockFs({ + '.jsdocrc.json': '\uFEFF{"foo":"bar"}' + }); + + const conf = config.loadSync(); + + expect(conf.config.foo).toBe('bar'); + }); + + it('parses YAML files that start with a BOM', () => { + mockFs({ + '.jsdocrc.yaml': '\uFEFF{"foo":"bar"}' + }); + + const conf = config.loadSync(); + + expect(conf.config.foo).toBe('bar'); + }); + + it('provides the default config if the user config is an empty object', () => { + mockFs({ + '.jsdocrc.json': '{}' + }); + + const conf = config.loadSync(); + + expect(conf.config).toEqual(config.defaults); + }); + + it('provides the default config if there is no user config', () => { + const conf = config.loadSync(); + + expect(conf.config).toEqual(config.defaults); + }); + + it('merges nested defaults with nested user settings as expected', () => { + mockFs({ + '.jsdocrc.json': '{"tags":{"foo":"bar"}}' + }); + + const conf = config.loadSync(); + + expect(conf.config.tags.allowUnknownTags).toBe(config.defaults.tags.allowUnknownTags); + expect(conf.config.tags.foo).toBe('bar'); + }); + }); + + describe('defaults', () => { + const { defaults } = config; + + it('exists', () => { + expect(defaults).toBeObject(); + }); + + describe('plugins', () => { + it('is an array', () => { + expect(defaults.plugins).toBeArray(); + }); + }); + + describe('source', () => { + it('is an object', () => { + expect(defaults.source).toBeObject(); + }); + + describe('excludePattern', () => { + it('is a string', () => { + expect(defaults.source.excludePattern).toBeString(); + }); + + it('represents a valid regexp', () => { + expect(() => new RegExp(defaults.source.excludePattern)).not.toThrow(); + }); + }); + + describe('includePattern', () => { + it('is a string', () => { + expect(defaults.source.includePattern).toBeString(); + }); + + it('represents a valid regexp', () => { + expect(() => new RegExp(defaults.source.includePattern)).not.toThrow(); + }); + }); + + describe('type', () => { + it('is a string', () => { + expect(defaults.source.type).toBeString(); + }); + }); + + describe('tags', () => { + it('is an object', () => { + expect(defaults.tags).toBeObject(); + }); + + describe('allowUnknownTags', () => { + it('is a boolean', () => { + expect(defaults.tags.allowUnknownTags).toBeBoolean(); + }); + }); + + describe('dictionaries', () => { + it('is an array of strings', () => { + expect(defaults.tags.dictionaries).toBeArrayOfStrings(); + }); + }); + }); + + describe('templates', () => { + it('is an object', () => { + expect(defaults.templates).toBeObject(); + }); + + describe('cleverLinks', () => { + it('is a boolean', () => { + expect(defaults.templates.cleverLinks).toBeBoolean(); + }); + }); + + describe('monospaceLinks', () => { + it('is a boolean', () => { + expect(defaults.templates.monospaceLinks).toBeBoolean(); + }); + }); + }); + }); + }); +});