Skip to content

General correction of glob patterns and config file locating #742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Oct 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 5 additions & 2 deletions src/lib/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -248,7 +248,10 @@ export class Application extends ChildableComponent<Application, AbstractCompone
*/
public expandInputFiles(inputFiles: string[] = []): string[] {
let files: string[] = [];
const exclude: Array<IMinimatch> = 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));
Expand Down
13 changes: 7 additions & 6 deletions src/lib/converter/context.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -93,7 +95,7 @@ export class Context {
/**
* The pattern that should be used to flag external source files.
*/
private externalPattern?: IMinimatch;
private externalPattern?: Array<IMinimatch>;

/**
* Create a new Context instance.
Expand All @@ -114,7 +116,7 @@ export class Context {
this.scope = project;

if (converter.externalPattern) {
this.externalPattern = new Minimatch(converter.externalPattern);
this.externalPattern = createMinimatch(converter.externalPattern);
}
}

Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions src/lib/converter/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ export class Converter extends ChildableComponent<Application, ConverterComponen

@Option({
name: 'externalPattern',
help: 'Define a pattern for files that should be considered being external.'
help: 'Define patterns for files that should be considered being external.',
type: ParameterType.Array
})
externalPattern!: string;
externalPattern!: Array<string>;

@Option({
name: 'includeDeclarations',
Expand Down
40 changes: 23 additions & 17 deletions src/lib/utils/options/readers/tsconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this line is responsible for making typedoc 0.14 a breaking change for my projects.

The path that I use for my config file is something like node_modules/my-config-project/config/typedoc.config.json

This worked fine in 0.13 but breaks in 0.14.

I now need to update the path on 60+ projects..

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);
}
}

Expand All @@ -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)) {
Expand Down
51 changes: 39 additions & 12 deletions src/lib/utils/options/readers/typedoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions src/lib/utils/paths.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
}
62 changes: 62 additions & 0 deletions src/test/utils.paths.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing tests!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure thing

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');
});
});
});