11'use babel' ;
22
3- /* eslint-disable import/extensions, import/ no-extraneous-dependencies */
3+ // eslint-disable-next-line import/no-extraneous-dependencies, import/extensions
44import { CompositeDisposable } from 'atom' ;
5- /* eslint-enable import/extensions, import/no-extraneous-dependencies */
65
7- let helpers = null ;
8- let path = null ;
6+ // Dependencies
7+ let fs ;
8+ let path ;
9+ let helpers ;
10+
11+ // Internal Variables
12+ let bundledCsslintPath ;
13+
14+ const loadDeps = ( ) => {
15+ if ( ! fs ) {
16+ fs = require ( 'fs-plus' ) ;
17+ }
18+ if ( ! path ) {
19+ path = require ( 'path' ) ;
20+ }
21+ if ( ! helpers ) {
22+ helpers = require ( 'atom-linter' ) ;
23+ }
24+ } ;
925
1026export default {
1127 activate ( ) {
12- require ( 'atom-package-deps' ) . install ( 'linter-csslint' ) ;
28+ this . idleCallbacks = new Set ( ) ;
29+ let depsCallbackID ;
30+ const installLinterCsslintDeps = ( ) => {
31+ this . idleCallbacks . delete ( depsCallbackID ) ;
32+ if ( ! atom . inSpecMode ( ) ) {
33+ require ( 'atom-package-deps' ) . install ( 'linter-csslint' ) ;
34+ }
35+ loadDeps ( ) ;
36+
37+ // FIXME: Remove this after a few versions
38+ if ( atom . config . get ( 'linter-csslint.disableTimeout' ) ) {
39+ atom . config . unset ( 'linter-csslint.disableTimeout' ) ;
40+ }
41+ } ;
42+ depsCallbackID = window . requestIdleCallback ( installLinterCsslintDeps ) ;
43+ this . idleCallbacks . add ( depsCallbackID ) ;
1344
1445 this . subscriptions = new CompositeDisposable ( ) ;
1546 this . subscriptions . add (
16- atom . config . observe ( 'linter-csslint.disableTimeout ' , ( value ) => {
17- this . disableTimeout = value ;
47+ atom . config . observe ( 'linter-csslint.executablePath ' , ( value ) => {
48+ this . executablePath = value ;
1849 } ) ,
1950 ) ;
2051 } ,
2152
2253 deactivate ( ) {
54+ this . idleCallbacks . forEach ( callbackID => window . cancelIdleCallback ( callbackID ) ) ;
55+ this . idleCallbacks . clear ( ) ;
2356 this . subscriptions . dispose ( ) ;
2457 } ,
2558
@@ -28,77 +61,129 @@ export default {
2861 name : 'CSSLint' ,
2962 grammarScopes : [ 'source.css' , 'source.html' ] ,
3063 scope : 'file' ,
31- lintOnFly : true ,
32- lint ( textEditor ) {
33- if ( ! helpers ) {
34- helpers = require ( 'atom-linter' ) ;
35- }
36- if ( ! path ) {
37- path = require ( 'path' ) ;
38- }
64+ lintsOnChange : false ,
65+ lint : async ( textEditor ) => {
66+ loadDeps ( ) ;
3967 const filePath = textEditor . getPath ( ) ;
4068 const text = textEditor . getText ( ) ;
41- if ( text . length === 0 ) {
42- return Promise . resolve ( [ ] ) ;
69+ if ( ! filePath || text . length === 0 ) {
70+ // Empty or unsaved file
71+ return [ ] ;
4372 }
44- const parameters = [ '--format=json' , '-' ] ;
45- const exec = path . join ( __dirname , '..' , 'node_modules' , 'atomlinter-csslint' , 'cli.js' ) ;
73+
74+ const parameters = [
75+ '--format=json' ,
76+ filePath ,
77+ ] ;
78+
4679 const projectPath = atom . project . relativizePath ( filePath ) [ 0 ] ;
4780 let cwd = projectPath ;
48- if ( ! ( cwd ) ) {
81+ if ( ! cwd ) {
4982 cwd = path . dirname ( filePath ) ;
5083 }
51- const options = { stdin : text , cwd } ;
52- if ( this . disableTimeout ) {
53- options . timeout = Infinity ;
84+
85+ const execOptions = {
86+ cwd,
87+ uniqueKey : `linter-csslint::${ filePath } ` ,
88+ timeout : 1000 * 30 , // 30 seconds
89+ ignoreExitCode : true ,
90+ } ;
91+
92+ const execPath = this . determineExecPath ( this . executablePath , projectPath ) ;
93+
94+ const output = await helpers . exec ( execPath , parameters , execOptions ) ;
95+
96+ if ( textEditor . getText ( ) !== text ) {
97+ // The editor contents have changed, tell Linter not to update
98+ return null ;
5499 }
55- return helpers . execNode ( exec , parameters , options ) . then ( ( output ) => {
56- if ( textEditor . getText ( ) !== text ) {
57- // The editor contents have changed, tell Linter not to update
58- return null ;
59- }
60100
61- const toReturn = [ ] ;
62- if ( output . length < 1 ) {
63- // No output, no errors
64- return toReturn ;
101+ const toReturn = [ ] ;
102+
103+ if ( output . length < 1 ) {
104+ // No output, no errors
105+ return toReturn ;
106+ }
107+
108+ let lintResult ;
109+ try {
110+ lintResult = JSON . parse ( output ) ;
111+ } catch ( e ) {
112+ const excerpt = 'Invalid response received from CSSLint, check ' +
113+ 'your console for more details.' ;
114+ return [ {
115+ severity : 'error' ,
116+ excerpt,
117+ location : {
118+ file : filePath ,
119+ position : helpers . generateRange ( textEditor , 0 ) ,
120+ } ,
121+ } ] ;
122+ }
123+
124+ if ( lintResult . messages . length < 1 ) {
125+ // Output, but no errors found
126+ return toReturn ;
127+ }
128+
129+ lintResult . messages . forEach ( ( data ) => {
130+ let line ;
131+ let col ;
132+ if ( ! ( data . line && data . col ) ) {
133+ // Use the file start if a location wasn't defined
134+ [ line , col ] = [ 0 , 0 ] ;
135+ } else {
136+ [ line , col ] = [ data . line - 1 , data . col - 1 ] ;
65137 }
66138
67- const lintResult = JSON . parse ( output ) ;
139+ const severity = data . type === 'error' ? 'error' : 'warning' ;
68140
69- if ( lintResult . messages . length < 1 ) {
70- // Output, but no errors found
71- return toReturn ;
141+ const msg = {
142+ severity,
143+ excerpt : data . message ,
144+ location : {
145+ file : filePath ,
146+ position : helpers . generateRange ( textEditor , line , col ) ,
147+ } ,
148+ } ;
149+ if ( data . rule . id && data . rule . desc ) {
150+ msg . details = `${ data . rule . desc } (${ data . rule . id } )` ;
151+ }
152+ if ( data . rule . url ) {
153+ msg . url = data . rule . url ;
72154 }
73155
74- lintResult . messages . forEach ( ( data ) => {
75- let line ;
76- let col ;
77- if ( ! ( data . line && data . col ) ) {
78- // Use the file start if a location wasn't defined
79- [ line , col ] = [ 0 , 0 ] ;
80- } else {
81- [ line , col ] = [ data . line - 1 , data . col - 1 ] ;
82- }
83-
84- const msg = {
85- type : data . type . charAt ( 0 ) . toUpperCase ( ) + data . type . slice ( 1 ) ,
86- text : data . message ,
87- filePath,
88- range : helpers . generateRange ( textEditor , line , col ) ,
89- } ;
90-
91- if ( data . rule . id && data . rule . desc ) {
92- msg . trace = [ {
93- type : 'Trace' ,
94- text : `[${ data . rule . id } ] ${ data . rule . desc } ` ,
95- } ] ;
96- }
97- toReturn . push ( msg ) ;
98- } ) ;
99- return toReturn ;
156+ toReturn . push ( msg ) ;
100157 } ) ;
158+
159+ return toReturn ;
101160 } ,
102161 } ;
103162 } ,
163+
164+ determineExecPath ( givenPath , projectPath ) {
165+ let execPath = givenPath ;
166+ if ( execPath === '' ) {
167+ // Use the bundled copy of CSSLint
168+ let relativeBinPath = path . join ( 'node_modules' , '.bin' , 'csslint' ) ;
169+ if ( process . platform === 'win32' ) {
170+ relativeBinPath += '.cmd' ;
171+ }
172+ if ( ! bundledCsslintPath ) {
173+ const packagePath = atom . packages . resolvePackagePath ( 'linter-csslint' ) ;
174+ bundledCsslintPath = path . join ( packagePath , relativeBinPath ) ;
175+ }
176+ execPath = bundledCsslintPath ;
177+ if ( projectPath ) {
178+ const localCssLintPath = path . join ( projectPath , relativeBinPath ) ;
179+ if ( fs . existsSync ( localCssLintPath ) ) {
180+ execPath = localCssLintPath ;
181+ }
182+ }
183+ } else {
184+ // Normalize any usage of ~
185+ fs . normalize ( execPath ) ;
186+ }
187+ return execPath ;
188+ } ,
104189} ;
0 commit comments