forked from STRML/JSXHint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjsxhint.js
executable file
·267 lines (245 loc) · 8.48 KB
/
jsxhint.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* JSXHint CLI tool
*
* Copyright 2013 (c) Samuel Reed
* Inspired by and based on JSXHint by Conde Nast
*
* Please see LICENSE for details
*
*/
'use strict';
var fs = require('graceful-fs');
var path = require('path');
var react = require('react-tools');
try {
var babel = require('babel');
} catch(e) {
// ignore
}
var async = require('async');
var path = require('path');
var mkdirp = require('mkdirp');
var debug = require('debug')('jsxhint');
// Check map for copied support files (package.json, .jshintrc) for a speedup.
var checkedSupportFiles = {};
/**
* Transform a JSX file into a JS file for linting.
* @async
* @param {String} fileStream Readable stream containing file contents.
* @param {String} fileName Name of the file; "stdin" if reading from stdio.
* @param {Object} opts Options.
* @param {Function} cb The callback to call when it's ready.
*/
function transformJSX(fileStream, fileName, opts, cb){
function transformSource(source){
if (opts['--babel'] || opts['--babel-experimental']) {
return babel.transform(source, {experimental: opts['--babel-experimental'] || false}).code;
} else {
return react.transform(source, {harmony: opts['--harmony'], stripTypes: true});
}
}
function transformError(error){
if (opts['--babel'] || opts['--babel-experimental']) {
return {
file: fileName,
error: {
line: error.loc.line,
character: error.loc.column,
reason: error.stack.match(/.*:\s(.*)\s\(\d+:\d+\)/)[1],
code: 'E041'
}
};
} else {
return {
file: fileName,
error: {
line: error.lineNumber,
character: error.column,
reason: error.description,
code: 'E041'
}
};
}
}
function processFile(){
var hasExtension = /\.jsx$/.exec(fileName) || fileName === "stdin";
var err;
try {
if ((opts['--jsx-only'] && hasExtension) || !opts['--jsx-only']) {
source = transformSource(source);
}
} catch(e) {
// Only throw an error if this was definitely a jsx file.
// Seems that esprima has some problems with some js syntax.
if (hasExtension) {
console.error("Error while transforming jsx in file " + fileName + "\n", e.stack);
err = transformError(e);
}
} finally {
cb(err, source);
}
}
// Allow omitting filename
if (typeof fileName === "object"){
cb = arguments[2];
opts = arguments[1];
fileName = typeof fileStream === "string" ? fileStream : 'stdin';
}
if (!babel && (opts['--babel'] || opts['--babel-experimental'])) {
throw new Error("Optional babel parser not installed. Please `npm install [-g] babel`.");
}
// Allow passing strings into this method e.g. when using it as a lib
if (typeof fileStream === "string"){
fileStream = fs.createReadStream(fileStream, {encoding: "utf8"});
}
// Drain stream
var source = '';
fileStream.on('data', function(chunk){
source += chunk;
});
fileStream.on('end', processFile);
fileStream.on('error', cb);
}
/**
* Given a fileName, get a multi-platform compatible file path that can be appended to an existing filepath.
* This is not necessary on *nix but is important on Windows because absolutePath + absolutePath
* leads to the concatenation of a drive letter (`c:/`) which is a broken path.
* @param {String} fileName file name.
* @return {String} Cleaned file name.
*/
function getCleanAbsolutePath(fileName) {
return '/' + path.relative('/', path.resolve(fileName));
}
/**
* Find a config file, searching up from dir, and copy it to the tmpdir. The
* JSHint CLI uses these to determine settings.
* We attempt to preserve the original folder structure inside the tmpdir
* so that we have no unexpected configuration file priority.
* @param {String} dir Path
*/
function copyConfig(dir, file, cb){
var filePath = path.resolve(dir, file);
if (checkedSupportFiles[filePath]) return cb();
checkedSupportFiles[filePath] = true;
if (fs.existsSync(filePath)) {
var destination = path.join(exports.tmpdir, getCleanAbsolutePath(filePath));
var rs = fs.createReadStream(filePath);
var ws = fs.createWriteStream(destination);
debug("Copying support file from %s to temp directory.", filePath);
ws.on('close', cb);
// Indicate that this is copied already to prevent unnecessary file operations.
return rs.pipe(ws);
}
// Return null at the root. This is the case when dir and its parent are the same.
var parent = path.resolve(dir, '..');
return dir === parent ? cb() : copyConfig(parent, file, cb);
}
/**
* Given a filename and contents, write to disk.
* @private
* @param {String} fileName File name.
* @param {String} contents File contents.
* @param {Function} cb Callback.
*/
function createTempFile(fileName, contents, cb){
fileName = getCleanAbsolutePath(fileName);
var file = path.join(exports.tmpdir, fileName);
mkdirp(path.dirname(file), function(){
var dir = path.dirname(fileName);
// We need to write the file's contents to disk, but also grab
// its associated .jshintrc and package.json so that jshint can lint it
// with the proper settings.
async.parallel([
function(cb){ fs.createWriteStream(file).end(contents, cb); },
async.apply(copyConfig, dir, '.jshintrc'),
async.apply(copyConfig, dir, 'package.json')
], function(){ cb(null, file); });
});
}
/**
* Given a list of filenames and their contents, write temporary files
* to disk.
* @private
* @param {Array} fileNames File names.
* @param {Array} fileContents File contents.
* @param {Function} cb Callback.
*/
function createTempFiles(fileNames, fileContents, cb){
async.map(fileNames, function(fileName, cb){
createTempFile(fileName, fileContents[fileNames.indexOf(fileName)], cb);
}, cb);
}
/**
* Transform a list of files from jsx. Calls back with a map relating
* the new files (temp files) to the old file names, e.g.
* {tempFile: originalFileName}
*
* @param {Array} files File paths to transform.
* @param {Object} opts Options.
* @param {Function} cb Callback.
*/
function transformFiles(files, opts, cb){
async.map(files, function(fileStream, fileName, cb){
if (arguments.length === 2) {
cb = arguments[1];
fileName = arguments[0];
return transformJSX(fileName, opts, cb);
} else {
return transformJSX(fileStream, fileName, opts, cb);
}
}, function(err, fileContents){
if(err) return cb(err);
debug("Successfully transformed %d files to JSX.", files.length);
createTempFiles(files, fileContents, function(err, tempFileNames){
if(err) return cb(err);
debug("Moved %d files to temp directory at %s.", files.length, exports.tmpdir);
// Create map of temp file names to original file names
var fileNameMap = {};
files.forEach(function(fileName, index){
fileNameMap[tempFileNames[index]] = fileName;
});
cb(null, fileNameMap);
});
});
}
/**
* Given a stream (stdin), transform and save to a temporary file so it can be piped
* into JSHint.
* JSHint in stream mode attempts to read from process.stdin.
* Since we can't reload process.stdin with the new transformed data (without forking),
* we instead just write to a temp file and load it into JSHint.
*
* @param {ReadableStream} fileStream Readable stream containing data to transform.
* @param {Object} opts Options.
* @param {Function} cb Callback.
*/
function transformStream(fileStream, opts, cb){
transformJSX(fileStream, opts, function(err, contents){
if(err) return cb(err);
createTempFile(path.join(process.cwd(), 'stdin'), contents, function(noErr, tempFileName){
var out = {};
out[tempFileName] = 'stdin';
cb(null, out);
});
});
}
/**
* Called directly from cli.
* There are two ways files can be added; either they are specified on the cli
* or they are entered via stdin.
* If they are named from the cli, we need to treat them as globs.
*/
function run(files, opts, cb){
if (Array.isArray(files)){
transformFiles(files, opts, cb);
} else if (files instanceof require('stream').Readable){
transformStream(files, opts, cb);
} else {
throw new Error("Invalid input.");
}
}
exports.tmpdir = path.join(require('os').tmpdir(), 'jsxhint', String(process.pid));
exports.transformJSX = transformJSX;
exports.transformFiles = transformFiles;
exports.transformStream = transformStream;
exports.run = run;