diff --git a/src/index.ts b/src/index.ts index a1c3ca875..36c8fb168 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export { Application } from './lib/application'; export { CliApplication } from './lib/cli'; export { EventDispatcher, Event } from './lib/utils/events'; +export { createMinimatch } from './lib/utils/paths'; export { resetReflectionID } from './lib/models/reflections/abstract'; export { normalizePath } from './lib/utils/fs'; export * from './lib/models/reflections'; diff --git a/src/lib/application.ts b/src/lib/application.ts index 0fad877c7..83d1b817a 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -9,13 +9,13 @@ import * as Path from 'path'; import * as FS from 'fs'; import * as typescript from 'typescript'; -import { Minimatch, IMinimatch } from 'minimatch'; import { Converter } from './converter/index'; import { Renderer } from './output/renderer'; import { Serializer } from './serialization'; import { ProjectReflection } from './models/index'; import { Logger, ConsoleLogger, CallbackLogger, PluginHost, writeFile } from './utils/index'; +import { createMinimatch } from './utils/paths'; import { AbstractComponent, ChildableComponent, Component, Option, DUMMY_APPLICATION_OWNER } from './utils/component'; import { Options, OptionsReadMode, OptionsReadResult } from './utils/options/index'; @@ -248,7 +248,10 @@ export class Application extends ChildableComponent = this.exclude ? this.exclude.map(pattern => new Minimatch(pattern, {dot: true})) : []; + + const exclude = this.exclude + ? createMinimatch(this.exclude) + : []; function isExcluded(fileName: string): boolean { return exclude.some(mm => mm.match(fileName)); diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index 78e085f31..b2da96fb1 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -1,8 +1,10 @@ import * as ts from 'typescript'; -import { Minimatch, IMinimatch } from 'minimatch'; +import { IMinimatch } from 'minimatch'; import { Logger } from '../utils/loggers'; +import { createMinimatch } from '../utils/paths'; import { Reflection, ProjectReflection, ContainerReflection, Type } from '../models/index'; + import { createTypeParameter } from './factories/type-parameter'; import { Converter } from './converter'; @@ -93,7 +95,7 @@ export class Context { /** * The pattern that should be used to flag external source files. */ - private externalPattern?: IMinimatch; + private externalPattern?: Array; /** * Create a new Context instance. @@ -114,7 +116,7 @@ export class Context { this.scope = project; if (converter.externalPattern) { - this.externalPattern = new Minimatch(converter.externalPattern); + this.externalPattern = createMinimatch(converter.externalPattern); } } @@ -216,10 +218,9 @@ export class Context { * @param callback The callback that should be executed. */ withSourceFile(node: ts.SourceFile, callback: Function) { - const externalPattern = this.externalPattern; let isExternal = this.fileNames.indexOf(node.fileName) === -1; - if (externalPattern) { - isExternal = isExternal || externalPattern.match(node.fileName); + if (!isExternal && this.externalPattern) { + isExternal = this.externalPattern.some(mm => mm.match(node.fileName)); } if (isExternal && this.converter.excludeExternals) { diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index 9b65778ef..c22680ce6 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -42,9 +42,10 @@ export class Converter extends ChildableComponent; @Option({ name: 'includeDeclarations', diff --git a/src/lib/utils/options/readers/tsconfig.ts b/src/lib/utils/options/readers/tsconfig.ts index 02d44c75b..30bac190a 100644 --- a/src/lib/utils/options/readers/tsconfig.ts +++ b/src/lib/utils/options/readers/tsconfig.ts @@ -40,21 +40,32 @@ export class TSConfigReader extends OptionsComponent { return; } + let file: string | undefined; + if (TSConfigReader.OPTIONS_KEY in event.data) { - this.load(event, Path.resolve(event.data[TSConfigReader.OPTIONS_KEY])); + const tsconfig = event.data[TSConfigReader.OPTIONS_KEY]; + + if (/tsconfig\.json$/.test(tsconfig)) { + file = Path.resolve(tsconfig); + } else { + file = ts.findConfigFile(tsconfig, ts.sys.fileExists); + } + + if (!file || !FS.existsSync(file)) { + event.addError('The tsconfig file %s does not exist.', file || ''); + return; + } } else if (TSConfigReader.PROJECT_KEY in event.data) { // The `project` option may be a directory or file, so use TS to find it - const file = ts.findConfigFile(event.data[TSConfigReader.PROJECT_KEY], ts.sys.fileExists); - // If file is undefined, we found no file to load. - if (file) { - this.load(event, file); - } + file = ts.findConfigFile(event.data[TSConfigReader.PROJECT_KEY], ts.sys.fileExists); } else if (this.application.isCLI) { - const file = ts.findConfigFile('.', ts.sys.fileExists); - // If file is undefined, we found no file to load. - if (file) { - this.load(event, file); - } + // No file or directory has been specified so find the file in the root of the project + file = ts.findConfigFile('.', ts.sys.fileExists); + } + + // If file is undefined, we found no file to load. + if (file) { + this.load(event, file); } } @@ -65,14 +76,9 @@ export class TSConfigReader extends OptionsComponent { * @param fileName The absolute path and file name of the tsconfig file. */ load(event: DiscoverEvent, fileName: string) { - if (!FS.existsSync(fileName)) { - event.addError('The tsconfig file %s does not exist.', fileName); - return; - } - const { config } = ts.readConfigFile(fileName, ts.sys.readFile); if (config === undefined) { - event.addError('The tsconfig file %s does not contain valid JSON.', fileName); + event.addError('No valid tsconfig file found for %s.', fileName); return; } if (!_.isPlainObject(config)) { diff --git a/src/lib/utils/options/readers/typedoc.ts b/src/lib/utils/options/readers/typedoc.ts index abc0b8ec7..2ff821f1d 100644 --- a/src/lib/utils/options/readers/typedoc.ts +++ b/src/lib/utils/options/readers/typedoc.ts @@ -31,30 +31,57 @@ export class TypedocReader extends OptionsComponent { return; } + let file: string | undefined; + if (TypedocReader.OPTIONS_KEY in event.data) { - this.load(event, Path.resolve(event.data[TypedocReader.OPTIONS_KEY])); - } else if (this.application.isCLI) { - const file = Path.resolve('typedoc.js'); - if (FS.existsSync(file)) { - this.load(event, file); + let opts = event.data[TypedocReader.OPTIONS_KEY]; + + if (opts && opts[0] === '.') { + opts = Path.resolve(opts); + } + + file = this.findTypedocFile(opts); + + if (!file || !FS.existsSync(file)) { + event.addError('The options file could not be found with the given path %s.', opts); + return; } + } else if (this.application.isCLI) { + file = this.findTypedocFile(); + } + + file && this.load(event, file); + } + + /** + * Search for the typedoc.js or typedoc.json file from the given path + * + * @param path Path to the typedoc.(js|json) file. If path is a directory + * typedoc file will be attempted to be found at the root of this path + * @return the typedoc.(js|json) file path or undefined + */ + findTypedocFile(path: string = process.cwd()): string | undefined { + if (/typedoc\.js(on)?$/.test(path)) { + return path; + } + + let file = Path.join(path, 'typedoc.js'); + if (FS.existsSync(file)) { + return file; } + + file += 'on'; // look for JSON file + return FS.existsSync(file) ? file : undefined; } /** * Load the specified option file. * + * @param event The event object from the DISCOVER event. * @param optionFile The absolute path and file name of the option file. - * @param ignoreUnknownArgs Should unknown arguments be ignored? If so the parser - * will simply skip all unknown arguments. * @returns TRUE on success, otherwise FALSE. */ load(event: DiscoverEvent, optionFile: string) { - if (!FS.existsSync(optionFile)) { - event.addError('The option file %s does not exist.', optionFile); - return; - } - let data = require(optionFile); if (typeof data === 'function') { data = data(this.application); diff --git a/src/lib/utils/paths.ts b/src/lib/utils/paths.ts new file mode 100644 index 000000000..3f82b24db --- /dev/null +++ b/src/lib/utils/paths.ts @@ -0,0 +1,23 @@ +import * as Path from 'path'; +import { Minimatch, IMinimatch } from 'minimatch'; + +const unix = Path.sep === '/'; + +export function createMinimatch(patterns: string[]): IMinimatch[] { + return patterns.map((pattern: string): IMinimatch => { + // Ensure correct pathing on unix, by transforming `\` to `/` and remvoing any `X:/` fromt he path + if (unix) { pattern = pattern.replace(/[\\]/g, '/').replace(/^\w:/, ''); } + + // pattern paths not starting with '**' are resolved even if it is an + // absolute path, to ensure correct format for the current OS + if (pattern.substr(0, 2) !== '**') { + pattern = Path.resolve(pattern); + } + + // On Windows we transform `\` to `/` to unify the way paths are intepreted + if (!unix) { pattern = pattern.replace(/[\\]/g, '/'); } + + // Unify the path slashes before creating the minimatch, for more relyable matching + return new Minimatch(pattern, { dot: true }); + }); +} diff --git a/src/test/utils.paths.ts b/src/test/utils.paths.ts new file mode 100644 index 000000000..db9cae108 --- /dev/null +++ b/src/test/utils.paths.ts @@ -0,0 +1,62 @@ +import * as Path from 'path'; +import { Minimatch } from 'minimatch'; + +import isEqual = require('lodash/isEqual'); +import Assert = require('assert'); + +import { createMinimatch } from '..'; + +// Used to ensure uniform path cross OS +const absolutePath = (path: string) => Path.resolve(path.replace(/^\w:/, '')).replace(/[\\]/g, '/'); + +describe('Paths', () => { + describe('createMinimatch', () => { + it('Converts an array of paths to an array of Minimatch expressions', () => { + const mms = createMinimatch(['/some/path/**', '**/another/path/**', './relative/**/path']); + Assert(Array.isArray(mms), 'Didn\'t return an array'); + + const allAreMm = mms.every((mm) => mm instanceof Minimatch); + Assert(allAreMm, 'Not all paths are coverted to Minimatch'); + }); + + it('Minimatch can match absolute paths expressions', () => { + const paths = ['/unix/absolute/**/path', '\\windows\\alternative\\absolute\\path', 'C:\\Windows\\absolute\\*\\path', '**/arbitrary/path/**']; + const mms = createMinimatch(paths); + const patterns = mms.map(({ pattern }) => pattern); + const comparePaths = [ + absolutePath('/unix/absolute/**/path'), + absolutePath('/windows/alternative/absolute/path'), + absolutePath('/Windows/absolute/*/path'), + '**/arbitrary/path/**' + ]; + + Assert(isEqual(patterns, comparePaths), `Patterns have been altered:\nMMS: ${patterns}\nPaths: ${comparePaths}`); + + Assert(mms[0].match(absolutePath('/unix/absolute/some/sub/dir/path')), 'Din\'t match unix path'); + Assert(mms[1].match(absolutePath('/windows/alternative/absolute/path')), 'Din\'t match windows alternative path'); + Assert(mms[2].match(absolutePath('/Windows/absolute/test/path')), 'Din\'t match windows path'); + Assert(mms[3].match(absolutePath('/some/deep/arbitrary/path/leading/nowhere')), 'Din\'t match arbitrary path'); + }); + + it('Minimatch can match relative to the project root', () => { + const paths = ['./relative/**/path', '../parent/*/path', 'no/dot/relative/**/path/*', '*/subdir/**/path/*', '.dot/relative/**/path/*']; + const absPaths = paths.map((path) => absolutePath(path)); + const mms = createMinimatch(paths); + const patterns = mms.map(({ pattern }) => pattern); + + Assert(isEqual(patterns, absPaths), `Project root have not been added to paths:\nMMS: ${patterns}\nPaths: ${absPaths}`); + + Assert(mms[0].match(Path.resolve('relative/some/sub/dir/path')), 'Din\'t match relative path'); + Assert(mms[1].match(Path.resolve('../parent/dir/path')), 'Din\'t match parent path'); + Assert(mms[2].match(Path.resolve('no/dot/relative/some/sub/dir/path/test')), 'Din\'t match no dot path'); + Assert(mms[3].match(Path.resolve('some/subdir/path/here')), 'Din\'t match single star path'); + Assert(mms[4].match(Path.resolve('.dot/relative/some/sub/dir/path/test')), 'Din\'t match dot path'); + }); + + it('Minimatch matches dot files', () => { + const mm = createMinimatch(['/some/path/**'])[0]; + Assert(mm.match(absolutePath('/some/path/.dot/dir')), 'Didn\'t match .dot path'); + Assert(mm.match(absolutePath('/some/path/normal/dir')), 'Didn\'t match normal path'); + }); + }); +});