Skip to content

Commit dbdbfca

Browse files
authored
Merge pull request #742 from Tokimon/fix/paths
General correction of glob patterns and config file locating
2 parents 0faf3a5 + e5ed318 commit dbdbfca

8 files changed

Lines changed: 163 additions & 39 deletions

File tree

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { Application } from './lib/application';
22
export { CliApplication } from './lib/cli';
33

44
export { EventDispatcher, Event } from './lib/utils/events';
5+
export { createMinimatch } from './lib/utils/paths';
56
export { resetReflectionID } from './lib/models/reflections/abstract';
67
export { normalizePath } from './lib/utils/fs';
78
export * from './lib/models/reflections';

src/lib/application.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
import * as Path from 'path';
1010
import * as FS from 'fs';
1111
import * as typescript from 'typescript';
12-
import { Minimatch, IMinimatch } from 'minimatch';
1312

1413
import { Converter } from './converter/index';
1514
import { Renderer } from './output/renderer';
1615
import { Serializer } from './serialization';
1716
import { ProjectReflection } from './models/index';
1817
import { Logger, ConsoleLogger, CallbackLogger, PluginHost, writeFile } from './utils/index';
18+
import { createMinimatch } from './utils/paths';
1919

2020
import { AbstractComponent, ChildableComponent, Component, Option, DUMMY_APPLICATION_OWNER } from './utils/component';
2121
import { Options, OptionsReadMode, OptionsReadResult } from './utils/options/index';
@@ -248,7 +248,10 @@ export class Application extends ChildableComponent<Application, AbstractCompone
248248
*/
249249
public expandInputFiles(inputFiles: string[] = []): string[] {
250250
let files: string[] = [];
251-
const exclude: Array<IMinimatch> = this.exclude ? this.exclude.map(pattern => new Minimatch(pattern, {dot: true})) : [];
251+
252+
const exclude = this.exclude
253+
? createMinimatch(this.exclude)
254+
: [];
252255

253256
function isExcluded(fileName: string): boolean {
254257
return exclude.some(mm => mm.match(fileName));

src/lib/converter/context.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as ts from 'typescript';
2-
import { Minimatch, IMinimatch } from 'minimatch';
2+
import { IMinimatch } from 'minimatch';
33

44
import { Logger } from '../utils/loggers';
5+
import { createMinimatch } from '../utils/paths';
56
import { Reflection, ProjectReflection, ContainerReflection, Type } from '../models/index';
7+
68
import { createTypeParameter } from './factories/type-parameter';
79
import { Converter } from './converter';
810

@@ -93,7 +95,7 @@ export class Context {
9395
/**
9496
* The pattern that should be used to flag external source files.
9597
*/
96-
private externalPattern?: IMinimatch;
98+
private externalPattern?: Array<IMinimatch>;
9799

98100
/**
99101
* Create a new Context instance.
@@ -114,7 +116,7 @@ export class Context {
114116
this.scope = project;
115117

116118
if (converter.externalPattern) {
117-
this.externalPattern = new Minimatch(converter.externalPattern);
119+
this.externalPattern = createMinimatch(converter.externalPattern);
118120
}
119121
}
120122

@@ -216,10 +218,9 @@ export class Context {
216218
* @param callback The callback that should be executed.
217219
*/
218220
withSourceFile(node: ts.SourceFile, callback: Function) {
219-
const externalPattern = this.externalPattern;
220221
let isExternal = this.fileNames.indexOf(node.fileName) === -1;
221-
if (externalPattern) {
222-
isExternal = isExternal || externalPattern.match(node.fileName);
222+
if (!isExternal && this.externalPattern) {
223+
isExternal = this.externalPattern.some(mm => mm.match(node.fileName));
223224
}
224225

225226
if (isExternal && this.converter.excludeExternals) {

src/lib/converter/converter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ export class Converter extends ChildableComponent<Application, ConverterComponen
4242

4343
@Option({
4444
name: 'externalPattern',
45-
help: 'Define a pattern for files that should be considered being external.'
45+
help: 'Define patterns for files that should be considered being external.',
46+
type: ParameterType.Array
4647
})
47-
externalPattern!: string;
48+
externalPattern!: Array<string>;
4849

4950
@Option({
5051
name: 'includeDeclarations',

src/lib/utils/options/readers/tsconfig.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,32 @@ export class TSConfigReader extends OptionsComponent {
4040
return;
4141
}
4242

43+
let file: string | undefined;
44+
4345
if (TSConfigReader.OPTIONS_KEY in event.data) {
44-
this.load(event, Path.resolve(event.data[TSConfigReader.OPTIONS_KEY]));
46+
const tsconfig = event.data[TSConfigReader.OPTIONS_KEY];
47+
48+
if (/tsconfig\.json$/.test(tsconfig)) {
49+
file = Path.resolve(tsconfig);
50+
} else {
51+
file = ts.findConfigFile(tsconfig, ts.sys.fileExists);
52+
}
53+
54+
if (!file || !FS.existsSync(file)) {
55+
event.addError('The tsconfig file %s does not exist.', file || '');
56+
return;
57+
}
4558
} else if (TSConfigReader.PROJECT_KEY in event.data) {
4659
// The `project` option may be a directory or file, so use TS to find it
47-
const file = ts.findConfigFile(event.data[TSConfigReader.PROJECT_KEY], ts.sys.fileExists);
48-
// If file is undefined, we found no file to load.
49-
if (file) {
50-
this.load(event, file);
51-
}
60+
file = ts.findConfigFile(event.data[TSConfigReader.PROJECT_KEY], ts.sys.fileExists);
5261
} else if (this.application.isCLI) {
53-
const file = ts.findConfigFile('.', ts.sys.fileExists);
54-
// If file is undefined, we found no file to load.
55-
if (file) {
56-
this.load(event, file);
57-
}
62+
// No file or directory has been specified so find the file in the root of the project
63+
file = ts.findConfigFile('.', ts.sys.fileExists);
64+
}
65+
66+
// If file is undefined, we found no file to load.
67+
if (file) {
68+
this.load(event, file);
5869
}
5970
}
6071

@@ -65,14 +76,9 @@ export class TSConfigReader extends OptionsComponent {
6576
* @param fileName The absolute path and file name of the tsconfig file.
6677
*/
6778
load(event: DiscoverEvent, fileName: string) {
68-
if (!FS.existsSync(fileName)) {
69-
event.addError('The tsconfig file %s does not exist.', fileName);
70-
return;
71-
}
72-
7379
const { config } = ts.readConfigFile(fileName, ts.sys.readFile);
7480
if (config === undefined) {
75-
event.addError('The tsconfig file %s does not contain valid JSON.', fileName);
81+
event.addError('No valid tsconfig file found for %s.', fileName);
7682
return;
7783
}
7884
if (!_.isPlainObject(config)) {

src/lib/utils/options/readers/typedoc.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,57 @@ export class TypedocReader extends OptionsComponent {
3131
return;
3232
}
3333

34+
let file: string | undefined;
35+
3436
if (TypedocReader.OPTIONS_KEY in event.data) {
35-
this.load(event, Path.resolve(event.data[TypedocReader.OPTIONS_KEY]));
36-
} else if (this.application.isCLI) {
37-
const file = Path.resolve('typedoc.js');
38-
if (FS.existsSync(file)) {
39-
this.load(event, file);
37+
let opts = event.data[TypedocReader.OPTIONS_KEY];
38+
39+
if (opts && opts[0] === '.') {
40+
opts = Path.resolve(opts);
41+
}
42+
43+
file = this.findTypedocFile(opts);
44+
45+
if (!file || !FS.existsSync(file)) {
46+
event.addError('The options file could not be found with the given path %s.', opts);
47+
return;
4048
}
49+
} else if (this.application.isCLI) {
50+
file = this.findTypedocFile();
51+
}
52+
53+
file && this.load(event, file);
54+
}
55+
56+
/**
57+
* Search for the typedoc.js or typedoc.json file from the given path
58+
*
59+
* @param path Path to the typedoc.(js|json) file. If path is a directory
60+
* typedoc file will be attempted to be found at the root of this path
61+
* @return the typedoc.(js|json) file path or undefined
62+
*/
63+
findTypedocFile(path: string = process.cwd()): string | undefined {
64+
if (/typedoc\.js(on)?$/.test(path)) {
65+
return path;
66+
}
67+
68+
let file = Path.join(path, 'typedoc.js');
69+
if (FS.existsSync(file)) {
70+
return file;
4171
}
72+
73+
file += 'on'; // look for JSON file
74+
return FS.existsSync(file) ? file : undefined;
4275
}
4376

4477
/**
4578
* Load the specified option file.
4679
*
80+
* @param event The event object from the DISCOVER event.
4781
* @param optionFile The absolute path and file name of the option file.
48-
* @param ignoreUnknownArgs Should unknown arguments be ignored? If so the parser
49-
* will simply skip all unknown arguments.
5082
* @returns TRUE on success, otherwise FALSE.
5183
*/
5284
load(event: DiscoverEvent, optionFile: string) {
53-
if (!FS.existsSync(optionFile)) {
54-
event.addError('The option file %s does not exist.', optionFile);
55-
return;
56-
}
57-
5885
let data = require(optionFile);
5986
if (typeof data === 'function') {
6087
data = data(this.application);

src/lib/utils/paths.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Path from 'path';
2+
import { Minimatch, IMinimatch } from 'minimatch';
3+
4+
const unix = Path.sep === '/';
5+
6+
export function createMinimatch(patterns: string[]): IMinimatch[] {
7+
return patterns.map((pattern: string): IMinimatch => {
8+
// Ensure correct pathing on unix, by transforming `\` to `/` and remvoing any `X:/` fromt he path
9+
if (unix) { pattern = pattern.replace(/[\\]/g, '/').replace(/^\w:/, ''); }
10+
11+
// pattern paths not starting with '**' are resolved even if it is an
12+
// absolute path, to ensure correct format for the current OS
13+
if (pattern.substr(0, 2) !== '**') {
14+
pattern = Path.resolve(pattern);
15+
}
16+
17+
// On Windows we transform `\` to `/` to unify the way paths are intepreted
18+
if (!unix) { pattern = pattern.replace(/[\\]/g, '/'); }
19+
20+
// Unify the path slashes before creating the minimatch, for more relyable matching
21+
return new Minimatch(pattern, { dot: true });
22+
});
23+
}

src/test/utils.paths.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as Path from 'path';
2+
import { Minimatch } from 'minimatch';
3+
4+
import isEqual = require('lodash/isEqual');
5+
import Assert = require('assert');
6+
7+
import { createMinimatch } from '..';
8+
9+
// Used to ensure uniform path cross OS
10+
const absolutePath = (path: string) => Path.resolve(path.replace(/^\w:/, '')).replace(/[\\]/g, '/');
11+
12+
describe('Paths', () => {
13+
describe('createMinimatch', () => {
14+
it('Converts an array of paths to an array of Minimatch expressions', () => {
15+
const mms = createMinimatch(['/some/path/**', '**/another/path/**', './relative/**/path']);
16+
Assert(Array.isArray(mms), 'Didn\'t return an array');
17+
18+
const allAreMm = mms.every((mm) => mm instanceof Minimatch);
19+
Assert(allAreMm, 'Not all paths are coverted to Minimatch');
20+
});
21+
22+
it('Minimatch can match absolute paths expressions', () => {
23+
const paths = ['/unix/absolute/**/path', '\\windows\\alternative\\absolute\\path', 'C:\\Windows\\absolute\\*\\path', '**/arbitrary/path/**'];
24+
const mms = createMinimatch(paths);
25+
const patterns = mms.map(({ pattern }) => pattern);
26+
const comparePaths = [
27+
absolutePath('/unix/absolute/**/path'),
28+
absolutePath('/windows/alternative/absolute/path'),
29+
absolutePath('/Windows/absolute/*/path'),
30+
'**/arbitrary/path/**'
31+
];
32+
33+
Assert(isEqual(patterns, comparePaths), `Patterns have been altered:\nMMS: ${patterns}\nPaths: ${comparePaths}`);
34+
35+
Assert(mms[0].match(absolutePath('/unix/absolute/some/sub/dir/path')), 'Din\'t match unix path');
36+
Assert(mms[1].match(absolutePath('/windows/alternative/absolute/path')), 'Din\'t match windows alternative path');
37+
Assert(mms[2].match(absolutePath('/Windows/absolute/test/path')), 'Din\'t match windows path');
38+
Assert(mms[3].match(absolutePath('/some/deep/arbitrary/path/leading/nowhere')), 'Din\'t match arbitrary path');
39+
});
40+
41+
it('Minimatch can match relative to the project root', () => {
42+
const paths = ['./relative/**/path', '../parent/*/path', 'no/dot/relative/**/path/*', '*/subdir/**/path/*', '.dot/relative/**/path/*'];
43+
const absPaths = paths.map((path) => absolutePath(path));
44+
const mms = createMinimatch(paths);
45+
const patterns = mms.map(({ pattern }) => pattern);
46+
47+
Assert(isEqual(patterns, absPaths), `Project root have not been added to paths:\nMMS: ${patterns}\nPaths: ${absPaths}`);
48+
49+
Assert(mms[0].match(Path.resolve('relative/some/sub/dir/path')), 'Din\'t match relative path');
50+
Assert(mms[1].match(Path.resolve('../parent/dir/path')), 'Din\'t match parent path');
51+
Assert(mms[2].match(Path.resolve('no/dot/relative/some/sub/dir/path/test')), 'Din\'t match no dot path');
52+
Assert(mms[3].match(Path.resolve('some/subdir/path/here')), 'Din\'t match single star path');
53+
Assert(mms[4].match(Path.resolve('.dot/relative/some/sub/dir/path/test')), 'Din\'t match dot path');
54+
});
55+
56+
it('Minimatch matches dot files', () => {
57+
const mm = createMinimatch(['/some/path/**'])[0];
58+
Assert(mm.match(absolutePath('/some/path/.dot/dir')), 'Didn\'t match .dot path');
59+
Assert(mm.match(absolutePath('/some/path/normal/dir')), 'Didn\'t match normal path');
60+
});
61+
});
62+
});

0 commit comments

Comments
 (0)