Skip to content

Commit 45e21e4

Browse files
committed
build: add path mapping support to broccoli typescript
1 parent ba120c2 commit 45e21e4

File tree

2 files changed

+151
-29
lines changed

2 files changed

+151
-29
lines changed

lib/broccoli/broccoli-typescript.js

Lines changed: 120 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,14 @@ class BroccoliTypeScriptCompiler extends Plugin {
159159

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

162-
this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig.compilerOptions, '', null).options;
162+
this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig['compilerOptions'],
163+
this.inputPaths[0], this._tsConfigPath).options;
163164
this._tsOpts.rootDir = '';
164165
this._tsOpts.outDir = '';
165166

166167
this._tsServiceHost = new CustomLanguageServiceHost(
167-
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0]);
168+
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0],
169+
tsconfig['compilerOptions'].paths, this._tsConfigPath);
168170
this._tsService = ts.createLanguageService(this._tsServiceHost, ts.createDocumentRegistry());
169171
}
170172

@@ -249,13 +251,15 @@ class BroccoliTypeScriptCompiler extends Plugin {
249251
}
250252

251253
class CustomLanguageServiceHost {
252-
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath) {
254+
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath, paths, tsConfigPath) {
253255
this.compilerOptions = compilerOptions;
254256
this.fileNames = fileNames;
255257
this.fileRegistry = fileRegistry;
256258
this.treeInputPath = treeInputPath;
257259
this.currentDirectory = treeInputPath;
258260
this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions).replace(/\\/g, '/');
261+
this.paths = paths;
262+
this.tsConfigPath = tsConfigPath;
259263
this.projectVersion = 0;
260264
}
261265

@@ -272,23 +276,87 @@ class CustomLanguageServiceHost {
272276
return this.projectVersion.toString();
273277
}
274278

275-
/**
276-
* This method is called quite a bit to lookup 3 kinds of paths:
277-
* 1/ files in the fileRegistry
278-
* - these are the files in our project that we are watching for changes
279-
* - in the future we could add caching for these files and invalidate the cache when
280-
* the file is changed lazily during lookup
281-
* 2/ .d.ts and library files not in the fileRegistry
282-
* - these are not our files, they come from tsd or typescript itself
283-
* - these files change only rarely but since we need them very rarely, it's not worth the
284-
* cache invalidation hassle to cache them
285-
* 3/ bogus paths that typescript compiler tries to lookup during import resolution
286-
* - these paths are tricky to cache since files come and go and paths that was bogus in the
287-
* past might not be bogus later
288-
*
289-
* In the initial experiments the impact of this caching was insignificant (single digit %) and
290-
* not worth the potential issues with stale cache records.
291-
*/
279+
_resolveModulePathWithMapping(moduleName) {
280+
// check if module name should be used as-is or it should be mapped to different value
281+
let longestMatchedPrefixLength = 0;
282+
let matchedPattern;
283+
let matchedWildcard;
284+
const paths = this.paths || {};
285+
286+
for (let pattern of Object.keys(paths)) {
287+
if (pattern.indexOf('*') != pattern.lastIndexOf('*')) {
288+
throw `Invalid path mapping pattern: "${pattern}"`;
289+
}
290+
291+
let indexOfWildcard = pattern.indexOf('*');
292+
if (indexOfWildcard !== -1) {
293+
// check if module name starts with prefix, ends with suffix and these two don't overlap
294+
let prefix = pattern.substr(0, indexOfWildcard);
295+
let suffix = pattern.substr(indexOfWildcard + 1);
296+
if (moduleName.length >= prefix.length + suffix.length &&
297+
moduleName.startsWith(prefix) &&
298+
moduleName.endsWith(suffix)) {
299+
300+
// use length of matched prefix as betterness criteria
301+
if (longestMatchedPrefixLength < prefix.length) {
302+
longestMatchedPrefixLength = prefix.length;
303+
matchedPattern = pattern;
304+
matchedWildcard = moduleName.substr(prefix.length, moduleName.length - suffix.length);
305+
}
306+
}
307+
} else {
308+
// Pattern does not contain asterisk - module name should exactly match pattern to succeed.
309+
if (pattern === moduleName) {
310+
matchedPattern = pattern;
311+
matchedWildcard = undefined;
312+
break;
313+
}
314+
}
315+
}
316+
317+
if (!matchedPattern) {
318+
// We fallback to the old module resolution.
319+
return undefined;
320+
// // no pattern was matched so module name can be used as-is
321+
// let p = path.join(this.treeInputPath, moduleName);
322+
// return fs.existsSync(p) ? p : undefined;
323+
}
324+
325+
// some pattern was matched - module name needs to be substituted
326+
let substitutions = this.paths[matchedPattern];
327+
for (let subst of substitutions) {
328+
if (subst.indexOf('*') != subst.lastIndexOf('*')) {
329+
throw `Invalid substitution: "${subst}" for pattern "${matchedPattern}".`;
330+
}
331+
// replace * in substitution with matched wildcard
332+
let p = matchedWildcard ? subst.replace('*', matchedWildcard) : subst;
333+
// if substituion is a relative path - combine it with baseUrl
334+
p = path.isAbsolute(p) ? p : path.join(this.treeInputPath, path.dirname(this.tsConfigPath), p);
335+
if (fs.existsSync(p)) {
336+
return p;
337+
}
338+
}
339+
340+
return undefined;
341+
}
342+
343+
// /**
344+
// * This method is called quite a bit to lookup 3 kinds of paths:
345+
// * 1/ files in the fileRegistry
346+
// * - these are the files in our project that we are watching for changes
347+
// * - in the future we could add caching for these files and invalidate the cache when
348+
// * the file is changed lazily during lookup
349+
// * 2/ .d.ts and library files not in the fileRegistry
350+
// * - these are not our files, they come from tsd or typescript itself
351+
// * - these files change only rarely but since we need them very rarely, it's not worth the
352+
// * cache invalidation hassle to cache them
353+
// * 3/ bogus paths that typescript compiler tries to lookup during import resolution
354+
// * - these paths are tricky to cache since files come and go and paths that was bogus in the
355+
// * past might not be bogus later
356+
// *
357+
// * In the initial experiments the impact of this caching was insignificant (single digit %) and
358+
// * not worth the potential issues with stale cache records.
359+
// */
292360
getScriptSnapshot(tsFilePath) {
293361
var absoluteTsFilePath;
294362
if (tsFilePath == this.defaultLibFilePath || path.isAbsolute(tsFilePath)) {
@@ -306,10 +374,41 @@ class CustomLanguageServiceHost {
306374
// so we we just return undefined when the path is not correct.
307375
return undefined;
308376
}
309-
310377
return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS));
311378
}
312379

380+
resolveModuleNames(moduleNames, containingFile)/*: ResolvedModule[]*/ {
381+
return moduleNames.map((moduleName) => {
382+
for (const ext of ['ts', 'd.ts']) {
383+
const name = `${moduleName}.${ext}`;
384+
const maybeModule = this._resolveModulePathWithMapping(name, containingFile);
385+
if (maybeModule) {
386+
return {
387+
resolvedFileName: maybeModule,
388+
isExternalLibraryImport: false
389+
};
390+
}
391+
}
392+
393+
return ts.resolveModuleName(moduleName, containingFile, this.compilerOptions, {
394+
fileExists(fileName) {
395+
return fs.existsSync(fileName);
396+
},
397+
readFile(fileName) {
398+
return fs.readFileSync(fileName, 'utf-8');
399+
},
400+
directoryExists(directoryName) {
401+
try {
402+
const stats = fs.statSync(directoryName);
403+
return stats && stats.isDirectory();
404+
} catch (e) {
405+
return false;
406+
}
407+
}
408+
}).resolvedModule;
409+
});
410+
}
411+
313412
getCurrentDirectory() {
314413
return this.currentDirectory;
315414
}

tests/e2e/e2e_workflow.spec.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,23 +315,46 @@ describe('Basic end-to-end Workflow', function () {
315315
let config = require(configFilePath);
316316

317317
config.compilerOptions.noImplicitAny = true;
318-
fs.writeFileSync(configFilePath, JSON.stringify(config), 'utf8');
318+
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');
319319

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

322322
return ng(['build'])
323-
.then(function () {
323+
.then(() => {
324324
expect(existsSync(path.join(process.cwd(), 'dist'))).to.be.equal(true);
325325
})
326326
.catch(() => {
327327
throw new Error('Build failed.');
328328
})
329-
.finally(function () {
330-
// Clean `tmp` folder
331-
process.chdir(path.resolve(root, '..'));
332-
// sh.rm('-rf', './tmp'); // tmp.teardown takes too long
333-
done();
334-
});
329+
.finally(done);
335330
});
336331

332+
it('Turn on path mapping in tsconfig.json and rebuild', function (done) {
333+
this.timeout(420000);
334+
335+
const configFilePath = path.join(process.cwd(), 'src', 'tsconfig.json');
336+
let config = require(configFilePath);
337+
338+
config.compilerOptions.baseUrl = '';
339+
340+
// This should fail.
341+
config.compilerOptions.paths = { '@angular/*': [] };
342+
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');
343+
344+
return ng(['build'])
345+
.then(() => {
346+
expect('build succeeded where it should have failed').to.equal('');
347+
})
348+
.catch(() => {})
349+
.then(() => {
350+
// This should succeed.
351+
config.compilerOptions.paths = { '@angular/*': [ '../../node_modules/@angular/*' ] };
352+
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');
353+
})
354+
.then(() => ng(['build']))
355+
.catch(() => {
356+
expect('build failed where it should have succeeded').to.equal('');
357+
})
358+
.finally(done);
359+
});
337360
});

0 commit comments

Comments
 (0)