Skip to content

Commit

Permalink
build: add path mapping support to broccoli typescript (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
hansl committed May 17, 2016
1 parent ba120c2 commit 289c9bd
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 14 deletions.
120 changes: 117 additions & 3 deletions lib/broccoli/broccoli-typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,14 @@ class BroccoliTypeScriptCompiler extends Plugin {

this._tsConfigFiles = tsconfig.files.splice(0);

this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig.compilerOptions, '', null).options;
this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig['compilerOptions'],
this.inputPaths[0], this._tsConfigPath).options;
this._tsOpts.rootDir = '';
this._tsOpts.outDir = '';

this._tsServiceHost = new CustomLanguageServiceHost(
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0]);
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0],
tsconfig['compilerOptions'].paths, this._tsConfigPath);
this._tsService = ts.createLanguageService(this._tsServiceHost, ts.createDocumentRegistry());
}

Expand Down Expand Up @@ -249,13 +251,15 @@ class BroccoliTypeScriptCompiler extends Plugin {
}

class CustomLanguageServiceHost {
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath) {
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath, paths, tsConfigPath) {
this.compilerOptions = compilerOptions;
this.fileNames = fileNames;
this.fileRegistry = fileRegistry;
this.treeInputPath = treeInputPath;
this.currentDirectory = treeInputPath;
this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions).replace(/\\/g, '/');
this.paths = paths;
this.tsConfigPath = tsConfigPath;
this.projectVersion = 0;
}

Expand All @@ -272,6 +276,80 @@ class CustomLanguageServiceHost {
return this.projectVersion.toString();
}

/**
* Resolve a moduleName based on the path mapping defined in the tsconfig.
* @param moduleName The module name to resolve.
* @returns {string|boolean} A string that is the path of the module, if found, or a boolean
* indicating if resolution should continue with default.
* @private
*/
_resolveModulePathWithMapping(moduleName) {
// check if module name should be used as-is or it should be mapped to different value
let longestMatchedPrefixLength = 0;
let matchedPattern;
let matchedWildcard;
const paths = this.paths || {};

for (let pattern of Object.keys(paths)) {
if (pattern.indexOf('*') != pattern.lastIndexOf('*')) {
throw `Invalid path mapping pattern: "${pattern}"`;
}

let indexOfWildcard = pattern.indexOf('*');
if (indexOfWildcard !== -1) {
// check if module name starts with prefix, ends with suffix and these two don't overlap
let prefix = pattern.substr(0, indexOfWildcard);
let suffix = pattern.substr(indexOfWildcard + 1);
if (moduleName.length >= prefix.length + suffix.length &&
moduleName.startsWith(prefix) &&
moduleName.endsWith(suffix)) {

// use length of matched prefix as betterness criteria
if (longestMatchedPrefixLength < prefix.length) {
longestMatchedPrefixLength = prefix.length;
matchedPattern = pattern;
matchedWildcard = moduleName.substr(prefix.length, moduleName.length - suffix.length);
}
}
} else {
// Pattern does not contain asterisk - module name should exactly match pattern to succeed.
if (pattern === moduleName) {
matchedPattern = pattern;
matchedWildcard = undefined;
break;
}
}
}

if (!matchedPattern) {
// We fallback to the old module resolution.
return true;
}

// some pattern was matched - module name needs to be substituted
let substitutions = this.paths[matchedPattern];
for (let subst of substitutions) {
if (subst.indexOf('*') != subst.lastIndexOf('*')) {
throw `Invalid substitution: "${subst}" for pattern "${matchedPattern}".`;
}
if (subst == '*') {
// Trigger default module resolution.
return true;
}
// replace * in substitution with matched wildcard
let p = matchedWildcard ? subst.replace('*', matchedWildcard) : subst;
// if substituion is a relative path - combine it with baseUrl
p = path.isAbsolute(p) ? p : path.join(this.treeInputPath, path.dirname(this.tsConfigPath), p);
if (fs.existsSync(p)) {
return p;
}
}

// This is an error; there was a match but no corresponding mapping was valid.
// Do not call the default module resolution.
return false;
}

/**
* This method is called quite a bit to lookup 3 kinds of paths:
* 1/ files in the fileRegistry
Expand Down Expand Up @@ -310,6 +388,42 @@ class CustomLanguageServiceHost {
return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS));
}

resolveModuleNames(moduleNames, containingFile)/*: ResolvedModule[]*/ {
return moduleNames.map((moduleName) => {
let shouldResolveUsingDefaultMethod = false;
for (const ext of ['ts', 'd.ts']) {
const name = `${moduleName}.${ext}`;
const maybeModule = this._resolveModulePathWithMapping(name, containingFile);
if (typeof maybeModule == 'string') {
return {
resolvedFileName: maybeModule,
isExternalLibraryImport: false
};
} else {
shouldResolveUsingDefaultMethod = shouldResolveUsingDefaultMethod || maybeModule;
}
}

return shouldResolveUsingDefaultMethod &&
ts.resolveModuleName(moduleName, containingFile, this.compilerOptions, {
fileExists(fileName) {
return fs.existsSync(fileName);
},
readFile(fileName) {
return fs.readFileSync(fileName, 'utf-8');
},
directoryExists(directoryName) {
try {
const stats = fs.statSync(directoryName);
return stats && stats.isDirectory();
} catch (e) {
return false;
}
}
}).resolvedModule;
});
}

getCurrentDirectory() {
return this.currentDirectory;
}
Expand Down
44 changes: 33 additions & 11 deletions tests/e2e/e2e_workflow.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,30 +308,52 @@ describe('Basic end-to-end Workflow', function () {
});
});

it('Turn on `noImplicitAny` in tsconfig.json and rebuild', function (done) {
it('Turn on `noImplicitAny` in tsconfig.json and rebuild', function () {
this.timeout(420000);

const configFilePath = path.join(process.cwd(), 'src', 'tsconfig.json');
let config = require(configFilePath);

config.compilerOptions.noImplicitAny = true;
fs.writeFileSync(configFilePath, JSON.stringify(config), 'utf8');
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');

sh.rm('-rf', path.join(process.cwd(), 'dist'));

return ng(['build'])
.then(function () {
.then(() => {
expect(existsSync(path.join(process.cwd(), 'dist'))).to.be.equal(true);
})
});
});

it('Turn on path mapping in tsconfig.json and rebuild', function () {
this.timeout(420000);

const configFilePath = path.join(process.cwd(), 'src', 'tsconfig.json');
let config = require(configFilePath);

config.compilerOptions.baseUrl = '';

// This should fail.
config.compilerOptions.paths = { '@angular/*': [] };
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');

return ng(['build'])
.catch(() => {
throw new Error('Build failed.');
return true;
})
.finally(function () {
// Clean `tmp` folder
process.chdir(path.resolve(root, '..'));
// sh.rm('-rf', './tmp'); // tmp.teardown takes too long
done();
.then((passed) => {
expect(passed).to.equal(true);
})
.then(() => {
// This should succeed.
config.compilerOptions.paths = {
'@angular/*': [ '*' ]
};
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');
})
.then(() => ng(['build']))
.catch(() => {
expect('build failed where it should have succeeded').to.equal('');
});
});

});

0 comments on commit 289c9bd

Please sign in to comment.