Skip to content

Commit 4ffdaa0

Browse files
committed
build: add path mapping support to broccoli typescript
1 parent 6b45099 commit 4ffdaa0

File tree

3 files changed

+149
-25
lines changed

3 files changed

+149
-25
lines changed

lib/broccoli/angular-broccoli-sass.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ class SASSPlugin extends Plugin {
3535

3636
compile(fileName, inputPath, outputPath) {
3737
const outSourceName = fileName.replace(inputPath, outputPath);
38-
const outFileName = outSourceName.replace(/\.s[ac]ss$/, '.css');
38+
let outFileName = outSourceName.replace(/\.s[ac]ss$/, '.css');
39+
if (this.options.getDestinationPath) {
40+
let tmpFileName = this.options.getDestinationPath(path.relative(outputPath, outFileName));
41+
if (tmpFileName) {
42+
outFileName = path.join(outputPath, tmpFileName);
43+
}
44+
}
3945

4046
// We overwrite file, outFile and include the file path for the includePath.
4147
// We also make sure the options don't include a data field.

lib/broccoli/angular2-app.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,18 @@ class Angular2App extends BroccoliPlugin {
379379
* @return {Tree} The assets tree.
380380
*/
381381
_getAssetsTree() {
382+
const getDestinationPath = (name) => {
383+
if (this._options.assets && this._options.assets.getDestinationPath) {
384+
name = this._options.assets.getDestinationPath(name);
385+
}
386+
if (name.startsWith(this._sourceDir)) {
387+
return name.substr(this._sourceDir.length);
388+
} else {
389+
return name;
390+
}
391+
};
392+
382393
return new BroccoliFunnel(this._inputNode, {
383-
srcDir: this._sourceDir,
384394
exclude: [
385395
'**/*.ts',
386396
'**/*.scss',
@@ -389,6 +399,7 @@ class Angular2App extends BroccoliPlugin {
389399
'**/*.styl',
390400
'**/tsconfig.json'
391401
],
402+
getDestinationPath: getDestinationPath,
392403
allowEmpty: true
393404
});
394405
}

lib/broccoli/broccoli-typescript.js

Lines changed: 130 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ class BroccoliTypeScriptCompiler extends Plugin {
7676
} else if (this._fileRegistry[tsFilePath].version >= entry.mtime) {
7777
// Nothing to do for this file. Just link the cached outputs.
7878
this._fileRegistry[tsFilePath].outputs.forEach(absoluteFilePath => {
79-
const outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath);
79+
let outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath);
80+
if (this._options.getDestinationPath) {
81+
const tmpOutputPath = this._options.getDestinationPath(path.relative(this.outputPath, outputFilePath));
82+
outputFilePath = tmpOutputPath ? path.join(this.outputPath, tmpOutputPath) : outputFilePath;
83+
}
8084
fse.mkdirsSync(path.dirname(outputFilePath));
8185
fs.symlinkSync(absoluteFilePath, outputFilePath);
8286
});
@@ -159,12 +163,14 @@ class BroccoliTypeScriptCompiler extends Plugin {
159163

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

162-
this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig.compilerOptions, '', null).options;
166+
this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig['compilerOptions'],
167+
this.inputPaths[0], this._tsConfigPath).options;
163168
this._tsOpts.rootDir = '';
164169
this._tsOpts.outDir = '';
165170

166171
this._tsServiceHost = new CustomLanguageServiceHost(
167-
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0]);
172+
this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0],
173+
tsconfig['compilerOptions'].paths, this._tsConfigPath);
168174
this._tsService = ts.createLanguageService(this._tsServiceHost, ts.createDocumentRegistry());
169175
}
170176

@@ -190,7 +196,11 @@ class BroccoliTypeScriptCompiler extends Plugin {
190196
absoluteFilePath = path.resolve(this.cachePath, absoluteFilePath);
191197
// Replace the input path by the output.
192198
absoluteFilePath = absoluteFilePath.replace(this.inputPaths[0], this.cachePath);
193-
const outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath);
199+
let outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath);
200+
if (this._options.getDestinationPath) {
201+
const tmpOutputPath = this._options.getDestinationPath(path.relative(this.outputPath, outputFilePath));
202+
outputFilePath = tmpOutputPath ? path.join(this.outputPath, tmpOutputPath) : outputFilePath;
203+
}
194204

195205
if (registry) {
196206
registry.outputs.add(absoluteFilePath);
@@ -249,13 +259,15 @@ class BroccoliTypeScriptCompiler extends Plugin {
249259
}
250260

251261
class CustomLanguageServiceHost {
252-
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath) {
262+
constructor(compilerOptions, fileNames, fileRegistry, treeInputPath, paths, tsConfigPath) {
253263
this.compilerOptions = compilerOptions;
254264
this.fileNames = fileNames;
255265
this.fileRegistry = fileRegistry;
256266
this.treeInputPath = treeInputPath;
257267
this.currentDirectory = treeInputPath;
258268
this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions).replace(/\\/g, '/');
269+
this.paths = paths;
270+
this.tsConfigPath = tsConfigPath;
259271
this.projectVersion = 0;
260272
}
261273

@@ -272,23 +284,87 @@ class CustomLanguageServiceHost {
272284
return this.projectVersion.toString();
273285
}
274286

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

388+
resolveModuleNames(moduleNames, containingFile)/*: ResolvedModule[]*/ {
389+
return moduleNames.map((moduleName) => {
390+
for (const ext of ['ts', 'd.ts']) {
391+
const name = `${moduleName}.${ext}`;
392+
const maybeModule = this._resolveModulePathWithMapping(name, containingFile);
393+
if (maybeModule) {
394+
return {
395+
resolvedFileName: maybeModule,
396+
isExternalLibraryImport: false
397+
};
398+
}
399+
}
400+
401+
return ts.resolveModuleName(moduleName, containingFile, this.compilerOptions, {
402+
fileExists(fileName) {
403+
return fs.existsSync(fileName);
404+
},
405+
readFile(fileName) {
406+
return fs.readFileSync(fileName, 'utf-8');
407+
},
408+
directoryExists(directoryName) {
409+
try {
410+
const stats = fs.statSync(directoryName);
411+
return stats && stats.isDirectory();
412+
} catch (e) {
413+
return false;
414+
}
415+
}
416+
}).resolvedModule;
417+
});
418+
}
419+
313420
getCurrentDirectory() {
314421
return this.currentDirectory;
315422
}

0 commit comments

Comments
 (0)