diff --git a/src/TypeScriptProgram.ts b/src/TypeScriptProgram.ts index abdeb5d..6d6bb00 100644 --- a/src/TypeScriptProgram.ts +++ b/src/TypeScriptProgram.ts @@ -1,5 +1,6 @@ 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 { @@ -7,9 +8,9 @@ export default class TypeScriptProgram { 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; @@ -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 @@ -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) { diff --git a/src/fileMatchesConfigGlob.ts b/src/fileMatchesConfigGlob.ts index 8e5482f..1def1d7 100644 --- a/src/fileMatchesConfigGlob.ts +++ b/src/fileMatchesConfigGlob.ts @@ -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 = importFile.substr( + 0, + importFile.length - path.extname(importFile).length + ); + return minimatch(importFile, normalizePath(configPath, key)); } diff --git a/src/fileMatchesTag.ts b/src/fileMatchesTag.ts index b603b0e..5c343ea 100644 --- a/src/fileMatchesTag.ts +++ b/src/fileMatchesTag.ts @@ -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; diff --git a/src/getAllConfigs.ts b/src/getAllConfigs.ts index 708a4d3..5a9bb71 100644 --- a/src/getAllConfigs.ts +++ b/src/getAllConfigs.ts @@ -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; @@ -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; }); } diff --git a/src/getConfigsForFile.ts b/src/getConfigsForFile.ts index 71595f0..389e5ea 100644 --- a/src/getConfigsForFile.ts +++ b/src/getConfigsForFile.ts @@ -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]) { diff --git a/src/getOptions.ts b/src/getOptions.ts index 5e7c36f..f63fb44 100644 --- a/src/getOptions.ts +++ b/src/getOptions.ts @@ -1,5 +1,5 @@ -import * as path from 'path'; import Options from './types/Options'; +import normalizePath from './normalizePath'; let options: Options; @@ -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'); } diff --git a/src/getTagsForFile.ts b/src/getTagsForFile.ts index f1cd669..fdd977f 100644 --- a/src/getTagsForFile.ts +++ b/src/getTagsForFile.ts @@ -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 } = {}; diff --git a/src/normalizePath.ts b/src/normalizePath.ts new file mode 100644 index 0000000..3b0ed4b --- /dev/null +++ b/src/normalizePath.ts @@ -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; +} diff --git a/src/runner.ts b/src/runner.ts index ebd8e61..4b3a047 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -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 @@ -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); }); } diff --git a/src/types/Config.ts b/src/types/Config.ts index 05bcc1c..40d4fb1 100644 --- a/src/types/Config.ts +++ b/src/types/Config.ts @@ -1,5 +1,7 @@ +import NormalizedPath from './NormalizedPath'; + export default interface Config { - path: string; + path: NormalizedPath; tags?: string[]; exports?: { [files: string]: string | string[] }; }; diff --git a/src/types/NormalizedPath.ts b/src/types/NormalizedPath.ts new file mode 100644 index 0000000..4034d03 --- /dev/null +++ b/src/types/NormalizedPath.ts @@ -0,0 +1,8 @@ +// Simulate nominal typing +// https://github.com/Microsoft/TypeScript/issues/202#issuecomment-302402671 +export declare class Nominal { + private nominalType: T; +} + +type NormalizedPath = string & Nominal<'Path'>; +export default NormalizedPath; diff --git a/src/types/Options.ts b/src/types/Options.ts index 7788d3a..a48cfc1 100644 --- a/src/types/Options.ts +++ b/src/types/Options.ts @@ -1,5 +1,7 @@ +import NormalizedPath from './NormalizedPath'; + export default interface Options { - project?: string; - rootDir?: string; + project?: NormalizedPath; + rootDir?: NormalizedPath; onError?: (message: string) => void; }; diff --git a/src/validateFile.ts b/src/validateFile.ts index 454a6dc..bc030ba 100644 --- a/src/validateFile.ts +++ b/src/validateFile.ts @@ -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)); } }); } diff --git a/src/validateImportIsAccessible.ts b/src/validateImportIsAccessible.ts index b92eef0..9ef7f41 100644 --- a/src/validateImportIsAccessible.ts +++ b/src/validateImportIsAccessible.ts @@ -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('..')) { @@ -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]; diff --git a/test/fileMatchesConfigGlobTests.ts b/test/fileMatchesConfigGlobTests.ts index be2f1ad..fb1763e 100644 --- a/test/fileMatchesConfigGlobTests.ts +++ b/test/fileMatchesConfigGlobTests.ts @@ -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', () => {