Skip to content

Commit

Permalink
Merge pull request #742 from Tokimon/fix/paths
Browse files Browse the repository at this point in the history
General correction of glob patterns and config file locating
  • Loading branch information
Gerrit0 committed Oct 28, 2018
2 parents 0faf3a5 + e5ed318 commit dbdbfca
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 39 deletions.
1 change: 1 addition & 0 deletions src/index.ts
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
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
@@ -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
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
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)) {
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
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
@@ -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
@@ -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');
});
});
});

0 comments on commit dbdbfca

Please sign in to comment.