Skip to content
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

Improve path handling #14

Merged
merged 9 commits into from
Nov 8, 2017
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
13 changes: 7 additions & 6 deletions src/TypeScriptProgram.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as path from 'path';
import * as ts from 'typescript';
import NormalizedPath from './types/NormalizedPath';

// Helper class for interacting with TypeScript
export default class TypeScriptProgram {
private compilerOptions: ts.CompilerOptions;
private compilerHost: ts.CompilerHost;
private program: ts.Program;

constructor(configFile: string) {
constructor(configFile: NormalizedPath) {
// Parse the config file
const projectPath = path.dirname(path.resolve(configFile));
const projectPath = path.dirname(configFile);
const config = readConfigFile(configFile);
const parsedConfig = ts.parseJsonConfigFileContent(config, ts.sys, projectPath);
this.compilerOptions = parsedConfig.options;
Expand All @@ -32,16 +33,16 @@ export default class TypeScriptProgram {
}

// Get all imports from a given file
getImportsForFile(fileName: string) {
getImportsForFile(fileName: NormalizedPath) {
let fileInfo = ts.preProcessFile(ts.sys.readFile(fileName), true, true);
return fileInfo.importedFiles;
}

// Resolve an imported module
resolveImportFromFile(moduleName: string, containingFile: string) {
resolveImportFromFile(moduleName: string, containingFile: NormalizedPath) {
const resolvedFile = ts.resolveModuleName(
moduleName,
containingFile,
containingFile.replace(/\\/g, '/'), // TypeScript doesn't like backslashes here
this.compilerOptions,
this.compilerHost,
null // TODO: provide a module resolution cache
Expand All @@ -51,7 +52,7 @@ export default class TypeScriptProgram {
}
}

function readConfigFile(configFile: string) {
function readConfigFile(configFile: NormalizedPath) {
const { config, error } = ts.readConfigFile(configFile, ts.sys.readFile);

if (error) {
Expand Down
15 changes: 12 additions & 3 deletions src/fileMatchesConfigGlob.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import * as path from 'path';
import NormalizedPath from './types/NormalizedPath';
import normalizePath from './normalizePath';
const minimatch = require('minimatch');

export default function fileMatchesConfigGlob(importFile: string, configPath: string, key: string) {
export default function fileMatchesConfigGlob(
importFile: NormalizedPath,
configPath: NormalizedPath,
key: string
) {
// '*' matches all files under the config
if (key == '*') {
return true;
}

// Remove the file extension before matching
importFile = importFile.substr(0, importFile.length - path.extname(importFile).length);
return minimatch(importFile, path.resolve(configPath, key));
importFile = <NormalizedPath>importFile.substr(
0,
importFile.length - path.extname(importFile).length
);
return minimatch(importFile, normalizePath(configPath, key));
}
3 changes: 2 additions & 1 deletion src/fileMatchesTag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import NormalizedPath from './types/NormalizedPath';
import getTagsForFile from './getTagsForFile';

// Returns true if the given file matches any of the given tags
export default function fileMatchesTag(filePath: string, tags: string | string[]) {
export default function fileMatchesTag(filePath: NormalizedPath, tags: string | string[]) {
// '*' matches all files
if (tags == '*') {
return true;
Expand Down
9 changes: 5 additions & 4 deletions src/getAllConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from 'fs';
import * as glob from 'glob';
import * as path from 'path';
import ConfigSet from './types/ConfigSet';
import normalizePath from './normalizePath';
import getOptions from './getOptions';

let configSet: ConfigSet = null;
Expand All @@ -11,11 +12,11 @@ export default function getAllConfigs(): ConfigSet {
configSet = {};

// Glob for configs under the project root directory
let files = glob.sync(path.resolve(getOptions().rootDir, '**/fence.json'));
let files = glob.sync(normalizePath(getOptions().rootDir, '**/fence.json'));
files.forEach(file => {
let absolutePath = path.resolve(path.dirname(file));
configSet[absolutePath] = JSON.parse(fs.readFileSync(file).toString());
configSet[absolutePath].path = absolutePath;
let configPath = normalizePath(path.dirname(file));
configSet[configPath] = JSON.parse(fs.readFileSync(file).toString());
configSet[configPath].path = configPath;
});
}

Expand Down
6 changes: 4 additions & 2 deletions src/getConfigsForFile.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import * as path from 'path';
import Config from './types/Config';
import NormalizedPath from './types/NormalizedPath';
import normalizePath from './normalizePath';
import getAllConfigs from './getAllConfigs';

// Returns an array of all the configs that apply to a given file
export default function getConfigsForFile(filePath: string): Config[] {
export default function getConfigsForFile(filePath: NormalizedPath): Config[] {
let allConfigs = getAllConfigs();
let configsForFile: Config[] = [];

let pathSegments = path.resolve(path.dirname(filePath)).split(path.sep);
let pathSegments = normalizePath(path.dirname(filePath)).split(path.sep);
while (pathSegments.length) {
let dirPath = pathSegments.join(path.sep);
if (allConfigs[dirPath]) {
Expand Down
8 changes: 4 additions & 4 deletions src/getOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from 'path';
import Options from './types/Options';
import normalizePath from './normalizePath';

let options: Options;

Expand All @@ -11,8 +11,8 @@ export function setOptions(providedOptions: Options) {
options = providedOptions;

// Normalize and apply defaults
options.rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve();
options.rootDir = normalizePath(options.rootDir || process.cwd());
options.project = options.project
? path.resolve(options.project)
: path.resolve(options.rootDir, 'tsconfig.json');
? normalizePath(options.project)
: normalizePath(options.rootDir, 'tsconfig.json');
}
3 changes: 2 additions & 1 deletion src/getTagsForFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import NormalizedPath from './types/NormalizedPath';
import getConfigsForFile from './getConfigsForFile';

export default function getTagsForFile(filePath: string): string[] {
export default function getTagsForFile(filePath: NormalizedPath): string[] {
let configs = getConfigsForFile(filePath);
let tags: { [tag: string]: boolean } = {};

Expand Down
14 changes: 14 additions & 0 deletions src/normalizePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as path from 'path';
import NormalizedPath from './types/NormalizedPath';

export default function normalizePath(...pathSegments: string[]) {
// Resolve the raw path to an absolute path
let normalizedPath = path.resolve.apply(null, pathSegments);

// Normalize drive letters to upper case
if (normalizedPath.match(/^[a-z]:/)) {
normalizedPath = normalizedPath.substr(0, 1).toUpperCase() + normalizedPath.substr(1);
}

return <NormalizedPath>normalizedPath;
}
3 changes: 2 additions & 1 deletion src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Options from './types/Options';
import getOptions, { setOptions } from './getOptions';
import validateFile from './validateFile';
import TypeScriptProgram from './TypeScriptProgram';
import normalizePath from './normalizePath';

export function run(options: Options) {
// Store options so they can be globally available
Expand All @@ -11,6 +12,6 @@ export function run(options: Options) {
let tsProgram = new TypeScriptProgram(getOptions().project);
let files = tsProgram.getSourceFiles();
files.forEach(file => {
validateFile(file, tsProgram);
validateFile(normalizePath(file), tsProgram);
});
}
4 changes: 3 additions & 1 deletion src/types/Config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import NormalizedPath from './NormalizedPath';

export default interface Config {
path: string;
path: NormalizedPath;
tags?: string[];
exports?: { [files: string]: string | string[] };
};
8 changes: 8 additions & 0 deletions src/types/NormalizedPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Simulate nominal typing
// https://github.com/Microsoft/TypeScript/issues/202#issuecomment-302402671
export declare class Nominal<T extends string> {
private nominalType: T;
}

type NormalizedPath = string & Nominal<'Path'>;
export default NormalizedPath;
6 changes: 4 additions & 2 deletions src/types/Options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import NormalizedPath from './NormalizedPath';

export default interface Options {
project?: string;
rootDir?: string;
project?: NormalizedPath;
rootDir?: NormalizedPath;
onError?: (message: string) => void;
};
6 changes: 4 additions & 2 deletions src/validateFile.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import NormalizedPath from './types/NormalizedPath';
import normalizePath from './normalizePath';
import TypeScriptProgram from './TypeScriptProgram';
import validateImportIsAccessible from './validateImportIsAccessible';

export default function validateFile(filePath: string, tsProgram: TypeScriptProgram) {
export default function validateFile(filePath: NormalizedPath, tsProgram: TypeScriptProgram) {
const importedFiles = tsProgram.getImportsForFile(filePath);
importedFiles.forEach(importInfo => {
const resolvedFileName = tsProgram.resolveImportFromFile(importInfo.fileName, filePath);
if (resolvedFileName) {
validateImportIsAccessible(filePath, resolvedFileName);
validateImportIsAccessible(filePath, normalizePath(resolvedFileName));
}
});
}
14 changes: 7 additions & 7 deletions src/validateImportIsAccessible.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import * as path from 'path';
import Config from './types/Config';
import NormalizedPath from './types/NormalizedPath';
import getConfigsForFile from './getConfigsForFile';
import fileMatchesConfigGlob from './fileMatchesConfigGlob';
import fileMatchesTag from './fileMatchesTag';
import reportError from './reportError';

export default function validateImportIsAccessible(sourceFile: string, importFile: string) {
// Make sure we're using absolute paths
sourceFile = path.resolve(sourceFile);
importFile = path.resolve(importFile);

export default function validateImportIsAccessible(
sourceFile: NormalizedPath,
importFile: NormalizedPath
) {
// Validate against each config that applies to the imported file
let configsForImport = getConfigsForFile(importFile);
configsForImport.forEach(config => validateConfig(config, sourceFile, importFile));
}

function validateConfig(config: Config, sourceFile: string, importFile: string) {
function validateConfig(config: Config, sourceFile: NormalizedPath, importFile: NormalizedPath) {
// If the source file is under the config (i.e. the source and import files share the
// config) then we don't apply the export rules
if (!path.relative(config.path, sourceFile).startsWith('..')) {
Expand All @@ -36,7 +36,7 @@ function validateConfig(config: Config, sourceFile: string, importFile: string)
reportError(`${sourceFile} is importing inaccessible module ${importFile}`);
}

function hasMatchingExport(config: Config, sourceFile: string, importFile: string) {
function hasMatchingExport(config: Config, sourceFile: NormalizedPath, importFile: NormalizedPath) {
let isExported = false;
Object.keys(config.exports).forEach(key => {
let tags = config.exports[key];
Expand Down
5 changes: 3 additions & 2 deletions test/fileMatchesConfigGlobTests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as path from 'path';
import normalizePath from '../src/normalizePath';
import fileMatchesConfigGlob from '../src/fileMatchesConfigGlob';

const importFilePath = path.resolve(normalize('a\\b\\c\\d\\e\\file.ts'));
const configPath = path.resolve(normalize('a\\b'));
const importFilePath = normalizePath(normalize('a\\b\\c\\d\\e\\file.ts'));
const configPath = normalizePath(normalize('a\\b'));

describe('fileMatchesConfigGlob', () => {
it('returns false if not a match', () => {
Expand Down