@@ -35,6 +35,10 @@ namespace ts.server {
3535 ( event : ProjectServiceEvent ) : void ;
3636 }
3737
38+ export interface SafeList {
39+ [ name : string ] : { match : RegExp , exclude ?: Array < Array < string | number > > , types ?: string [ ] } ;
40+ }
41+
3842 function prepareConvertersForEnumLikeCompilerOptions ( commandLineOptions : CommandLineOption [ ] ) : Map < Map < number > > {
3943 const map : Map < Map < number > > = createMap < Map < number > > ( ) ;
4044 for ( const option of commandLineOptions ) {
@@ -57,6 +61,32 @@ namespace ts.server {
5761 "smart" : IndentStyle . Smart
5862 } ) ;
5963
64+ const defaultTypeSafeList : SafeList = {
65+ "jquery" : {
66+ // jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js")
67+ "match" : / j q u e r y ( - ( \. ? \d + ) + ) ? ( \. i n t e l l i s e n s e ) ? ( \. m i n ) ? \. j s $ / i,
68+ "types" : [ "jquery" ]
69+ } ,
70+ "WinJS" : {
71+ // e.g. c:/temp/UWApp1/lib/winjs-4.0.1/js/base.js
72+ "match" : / ^ ( .* \/ w i n j s - [ . \d ] + ) \/ j s \/ b a s e \. j s $ / i, // If the winjs/base.js file is found..
73+ "exclude" : [ [ "^" , 1 , "/.*" ] ] , // ..then exclude all files under the winjs folder
74+ "types" : [ "winjs" ] // And fetch the @types package for WinJS
75+ } ,
76+ "Kendo" : {
77+ // e.g. /Kendo3/wwwroot/lib/kendo/kendo.all.min.js
78+ "match" : / ^ ( .* \/ k e n d o ) \/ k e n d o \. a l l \. m i n \. j s $ / i,
79+ "exclude" : [ [ "^" , 1 , "/.*" ] ] ,
80+ "types" : [ "kendo-ui" ]
81+ } ,
82+ "Office Nuget" : {
83+ // e.g. /scripts/Office/1/excel-15.debug.js
84+ "match" : / ^ ( .* \/ o f f i c e \/ 1 ) \/ e x c e l - \d + \. d e b u g \. j s $ / i, // Office NuGet package is installed under a "1/office" folder
85+ "exclude" : [ [ "^" , 1 , "/.*" ] ] , // Exclude that whole folder if the file indicated above is found in it
86+ "types" : [ "office" ] // @types package to fetch instead
87+ }
88+ } ;
89+
6090 export function convertFormatOptions ( protocolOptions : protocol . FormatCodeSettings ) : FormatCodeSettings {
6191 if ( typeof protocolOptions . indentStyle === "string" ) {
6292 protocolOptions . indentStyle = indentStyle . get ( protocolOptions . indentStyle . toLowerCase ( ) ) ;
@@ -259,6 +289,7 @@ namespace ts.server {
259289 private readonly throttledOperations : ThrottledOperations ;
260290
261291 private readonly hostConfiguration : HostConfiguration ;
292+ private static safelist : SafeList = defaultTypeSafeList ;
262293
263294 private changedFiles : ScriptInfo [ ] ;
264295
@@ -284,8 +315,6 @@ namespace ts.server {
284315
285316 this . typingsCache = new TypingsCache ( this . typingsInstaller ) ;
286317
287- // ts.disableIncrementalParsing = true;
288-
289318 this . hostConfiguration = {
290319 formatCodeOptions : getDefaultFormatCodeSettings ( this . host ) ,
291320 hostInfo : "Unknown host" ,
@@ -831,7 +860,7 @@ namespace ts.server {
831860 getDirectoryPath ( configFilename ) ,
832861 /*existingOptions*/ { } ,
833862 configFilename ,
834- /*resolutionStack*/ [ ] ,
863+ /*resolutionStack*/ [ ] ,
835864 this . hostConfiguration . extraFileExtensions ) ;
836865
837866 if ( parsedCommandLine . errors . length ) {
@@ -1399,13 +1428,104 @@ namespace ts.server {
13991428 this . refreshInferredProjects ( ) ;
14001429 }
14011430
1431+ /** Makes a filename safe to insert in a RegExp */
1432+ private static filenameEscapeRegexp = / [ - \/ \\ ^ $ * + ? . ( ) | [ \] { } ] / g;
1433+ private static escapeFilenameForRegex ( filename : string ) {
1434+ return filename . replace ( this . filenameEscapeRegexp , "\\$&" ) ;
1435+ }
1436+
1437+ resetSafeList ( ) : void {
1438+ ProjectService . safelist = defaultTypeSafeList ;
1439+ }
1440+
1441+ loadSafeList ( fileName : string ) : void {
1442+ const raw : SafeList = JSON . parse ( this . host . readFile ( fileName , "utf-8" ) ) ;
1443+ // Parse the regexps
1444+ for ( const k of Object . keys ( raw ) ) {
1445+ raw [ k ] . match = new RegExp ( raw [ k ] . match as { } as string , "i" ) ;
1446+ }
1447+ // raw is now fixed and ready
1448+ ProjectService . safelist = raw ;
1449+ }
1450+
1451+ applySafeList ( proj : protocol . ExternalProject ) : void {
1452+ const { rootFiles, typeAcquisition } = proj ;
1453+ const types = ( typeAcquisition && typeAcquisition . include ) || [ ] ;
1454+
1455+ const excludeRules : string [ ] = [ ] ;
1456+
1457+ const normalizedNames = rootFiles . map ( f => normalizeSlashes ( f . fileName ) ) ;
1458+
1459+ for ( const name of Object . keys ( ProjectService . safelist ) ) {
1460+ const rule = ProjectService . safelist [ name ] ;
1461+ for ( const root of normalizedNames ) {
1462+ if ( rule . match . test ( root ) ) {
1463+ this . logger . info ( `Excluding files based on rule ${ name } ` ) ;
1464+
1465+ // If the file matches, collect its types packages and exclude rules
1466+ if ( rule . types ) {
1467+ for ( const type of rule . types ) {
1468+ if ( types . indexOf ( type ) < 0 ) {
1469+ types . push ( type ) ;
1470+ }
1471+ }
1472+ }
1473+
1474+ if ( rule . exclude ) {
1475+ for ( const exclude of rule . exclude ) {
1476+ const processedRule = root . replace ( rule . match , ( ...groups : Array < string > ) => {
1477+ return exclude . map ( groupNumberOrString => {
1478+ // RegExp group numbers are 1-based, but the first element in groups
1479+ // is actually the original string, so it all works out in the end.
1480+ if ( typeof groupNumberOrString === "number" ) {
1481+ if ( typeof groups [ groupNumberOrString ] !== "string" ) {
1482+ // Specification was wrong - exclude nothing!
1483+ this . logger . info ( `Incorrect RegExp specification in safelist rule ${ name } - not enough groups` ) ;
1484+ // * can't appear in a filename; escape it because it's feeding into a RegExp
1485+ return "\\*" ;
1486+ }
1487+ return ProjectService . escapeFilenameForRegex ( groups [ groupNumberOrString ] ) ;
1488+ }
1489+ return groupNumberOrString ;
1490+ } ) . join ( "" ) ;
1491+ } ) ;
1492+
1493+ if ( excludeRules . indexOf ( processedRule ) === - 1 ) {
1494+ excludeRules . push ( processedRule ) ;
1495+ }
1496+ }
1497+ }
1498+ else {
1499+ // If not rules listed, add the default rule to exclude the matched file
1500+ const escaped = ProjectService . escapeFilenameForRegex ( root ) ;
1501+ if ( excludeRules . indexOf ( escaped ) < 0 ) {
1502+ excludeRules . push ( escaped ) ;
1503+ }
1504+ }
1505+ }
1506+ }
1507+
1508+ // Copy back this field into the project if needed
1509+ if ( types . length > 0 ) {
1510+ proj . typeAcquisition = proj . typeAcquisition || { } ;
1511+ proj . typeAcquisition . include = types ;
1512+ }
1513+ }
1514+
1515+ const excludeRegexes = excludeRules . map ( e => new RegExp ( e , "i" ) ) ;
1516+ proj . rootFiles = proj . rootFiles . filter ( ( _file , index ) => ! excludeRegexes . some ( re => re . test ( normalizedNames [ index ] ) ) ) ;
1517+ }
1518+
14021519 openExternalProject ( proj : protocol . ExternalProject , suppressRefreshOfInferredProjects = false ) : void {
14031520 // typingOptions has been deprecated and is only supported for backward compatibility
14041521 // purposes. It should be removed in future releases - use typeAcquisition instead.
14051522 if ( proj . typingOptions && ! proj . typeAcquisition ) {
14061523 const typeAcquisition = convertEnableAutoDiscoveryToEnable ( proj . typingOptions ) ;
14071524 proj . typeAcquisition = typeAcquisition ;
14081525 }
1526+
1527+ this . applySafeList ( proj ) ;
1528+
14091529 let tsConfigFiles : NormalizedPath [ ] ;
14101530 const rootFiles : protocol . ExternalFile [ ] = [ ] ;
14111531 for ( const file of proj . rootFiles ) {
0 commit comments