-
Notifications
You must be signed in to change notification settings - Fork 15
/
transformTools.coffee
328 lines (281 loc) · 12.9 KB
/
transformTools.coffee
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# Framework for building Falafel based transforms for Browserify.
path = require 'path'
fs = require 'fs'
through = require 'through'
falafel = require 'falafel'
loadConfig = require './loadConfig'
skipFile = require './skipFile'
exports.loadTransformConfig = loadConfig.loadTransformConfig
exports.loadTransformConfigSync = loadConfig.loadTransformConfigSync
exports.skipFile = skipFile
# TODO: Does this work on Windows?
isRootDir = (filename) -> filename == path.resolve(filename, '/')
merge = (a={}, b={}) ->
answer = {}
answer[key] = a[key] for key of a
answer[key] = b[key] for key of b
return answer
# Create a new Browserify transform which reads and returns a string.
#
# Browserify transforms work on streams. This is all well and good, until you want to call
# a library like "falafel" which doesn't work with streams.
#
# Suppose you are writing a transform called "redify" which replaces all occurances of "blue"
# with "red":
#
# options = {}
# module.exports = makeStringTransform "redify", options, (contents, transformOptions, done) ->
# done null, contents.replace(/blue/g, "red")
#
# Parameters:
# * `transformFn(contents, transformOptions, done)` - Function which is called to
# do the transform. `contents` are the contents of the file. `transformOptions.file` is the
# name of the file (as would be passed to a normal browserify transform.)
# `transformOptions.configData` is the configuration data for the transform (see
# `loadTransformConfig` below for details.) `transformOptions.config` is a copy of
# `transformOptions.configData.config` for convenience. `done(err, transformed)` is a callback
# which must be called, passing the a string with the transformed contents of the file.
# * `options.excludeExtensions` - A list of extensions which will not be processed. e.g.
# "['.coffee', '.jade']"
# * `options.includeExtensions` - A list of extensions to process. If this options is not
# specified, then all extensions will be processed. If this option is specified, then
# any file with an extension not in this list will skipped.
# * `options.jsFilesOnly` - If true (and if includeExtensions is not set) then this transform
# will only operate on .js files, and on files which are commonly compiled to javascript
# (.coffee, .litcoffee, .coffee.md, .jade, etc...)
#
exports.makeStringTransform = (transformName, options={}, transformFn) ->
if !transformFn?
transformFn = options
options = {}
transform = (file, config) ->
configData = if transform.configData?
transform.configData
else
loadConfig.loadTransformConfigSync transformName, file, options
if config?
configData ?= {config:{}}
configData.config = merge configData.config, config
if skipFile file, configData, options then return through()
# Read the file contents into `content`
content = ''
write = (buf) -> content += buf
# Called when we're done reading file contents
end = ->
handleError = (error) =>
suffix = " (while #{transformName} was processing #{file})"
if error instanceof Error and error.message
error.message += suffix
else
error = new Error("#{error}#{suffix}")
@emit 'error', error
try
transformOptions = {
file: file,
configData: configData,
config: configData?.config,
opts: configData?.config
}
transformFn.call @, content, transformOptions, (err, transformed) =>
return handleError err if err
@queue String(transformed)
@queue null
catch err
handleError err
return through write, end
# Called to manually pass configuration data to the transform. Configuration passed in this
# way will override configuration loaded from package.json.
#
# * `config` is the configuration data.
# * `configOptions.configFile` is the file that configuration data was loaded from. If this
# is specified and `configOptions.configDir` is not specified, then `configOptions.configDir`
# will be inferred from the configFile's path.
# * `configOptions.configDir` is the directory the configuration was loaded from. This is used
# by some transforms to resolve relative paths.
#
# Returns a new transform that uses the configuration:
#
# myTransform = require('myTransform').configure(...)
#
transform.configure = (config, configOptions = {}) ->
answer = exports.makeStringTransform transformName, options, transformFn
answer.setConfig config, configOptions
return answer
# Similar to `configure()`, but modifies the transform instance it is called on. This can
# be used to set the default configuration for the transform.
transform.setConfig = (config, configOptions = {}) ->
configFile = configOptions.configFile or null
configDir = configOptions.configDir or if configFile then path.dirname configFile else null
if !config
@configData = null
else
@configData = {
config: config,
configFile: configFile,
configDir: configDir,
cached: false
}
if config.appliesTo
@configData.appliesTo = config.appliesTo
delete config.appliesTo
return this
return transform
# Create a new Browserify transform based on [falafel](https://github.com/substack/node-falafel).
#
# Parameters:
# * `transformFn(node, transformOptions, done)` is called once for each falafel node. transformFn
# is free to update the falafel node directly; any value returned via `done(err)` is ignored.
# * `options.falafelOptions` are options to pass directly to Falafel.
# * `transformName`, `options.excludeExtensions`, `options.includeExtensions`, `options.jsFilesOnly`,
# and `tranformOptions` are the same as for `makeStringTransform()`.
#
exports.makeFalafelTransform = (transformName, options={}, transformFn) ->
if !transformFn?
transformFn = options
options = {}
falafelOptions = options.falafelOptions ? {}
transform = exports.makeStringTransform transformName, options, (content, transformOptions, done) ->
transformErr = null
pending = 1 # We'll decrement this to zero at the end to prevent premature call of `done`.
transformed = null
transformCb = (err) ->
if err and !transformErr
transformErr = err
done err
# Stop further processing if an error has occurred
return if transformErr
pending--
if pending is 0
done null, transformed
transformed = falafel content, falafelOptions, (node) ->
pending++
try
transformFn node, transformOptions, transformCb
catch err
transformCb err
# call transformCb one more time to decrement pending to 0.
transformCb transformErr, transformed
# Called to manually pass configuration data to the transform. Configuration passed in this
# way will override configuration loaded from package.json.
#
# * `config` is the configuration data.
# * `configOptions.configFile` is the file that configuration data was loaded from. If this
# is specified and `configOptions.configDir` is not specified, then `configOptions.configDir`
# will be inferred from the configFile's path.
# * `configOptions.configDir` is the directory the configuration was loaded from. This is used
# by some transforms to resolve relative paths.
#
# Returns a new transform that uses the configuration:
#
# myTransform = require('myTransform').configure(...)
#
transform.configure = (config, configOptions = {}) ->
answer = exports.makeFalafelTransform transformName, options, transformFn
answer.setConfig config, configOptions
return answer
return transform
# Create a new Browserify transform that modifies requires() calls.
#
# The resulting transform will call `transformFn(requireArgs, tranformOptions, cb)` for every
# requires in a file. transformFn should call `cb(null, str)` with a string which will replace the
# entire `require` call.
#
# Exmaple:
#
# makeRequireTransform "xify", (requireArgs, cb) ->
# cb null, "require(x" + requireArgs[0] + ")"
#
# would transform calls like `require("foo")` into `require("xfoo")`.
#
# `transformName`, `options.excludeExtensions`, `options.includeExtensions`, `options.jsFilesOnly`,
# and `tranformOptions` are the same as for `makeStringTransform()`.
#
# By default, makeRequireTransform will attempt to evaluate each "require" parameters.
# makeRequireTransform can handle variabls `__filename`, `__dirname`, `path`, and `join` (where
# `join` is treated as `path.join`) as well as any basic JS expressions. If the argument is
# too complicated to parse, then makeRequireTransform will return the source for the argument.
# You can disable parsing by passing `options.evaluateArguments` as false.
#
exports.makeRequireTransform = (transformName, options={}, transformFn) ->
if !transformFn?
transformFn = options
options = {}
evaluateArguments = options.evaluateArguments ? true
transform = exports.makeFalafelTransform transformName, options, (node, transformOptions, done) ->
if (node.type is 'CallExpression' and node.callee.type is 'Identifier' and
node.callee.name is 'require')
# Parse arguemnts to calls to `require`.
if evaluateArguments
# Based on https://github.com/ForbesLindesay/rfileify.
dirname = path.dirname(transformOptions.file)
varNames = ['__filename', '__dirname', 'path', 'join']
vars = [transformOptions.file, dirname, path, path.join]
args = node.arguments.map (arg) ->
t = "return #{arg.source()}"
try
return Function(varNames, t).apply(null, vars)
catch err
# Can't evaluate the arguemnts. Return the raw source.
return arg.source()
else
args = (arg.source() for arg in node.arguments)
transformFn args, transformOptions, (err, transformed) ->
return done err if err
if transformed? then node.update(transformed)
done()
else
done()
# Called to manually pass configuration data to the transform. Configuration passed in this
# way will override configuration loaded from package.json.
#
# * `config` is the configuration data.
# * `configOptions.configFile` is the file that configuration data was loaded from. If this
# is specified and `configOptions.configDir` is not specified, then `configOptions.configDir`
# will be inferred from the configFile's path.
# * `configOptions.configDir` is the directory the configuration was loaded from. This is used
# by some transforms to resolve relative paths.
#
# Returns a new transform that uses the configuration:
#
# myTransform = require('myTransform').configure(...)
#
transform.configure = (config, configOptions = {}) ->
answer = exports.makeRequireTransform transformName, options, transformFn
answer.setConfig config, configOptions
return answer
return transform
# Runs a Browserify-style transform on the given file.
#
# * `transform` is the transform to run (i.e. a `fn(file)` which returns a through stream.)
# * `file` is the name of the file to run the transform on.
# * `options.content` is the content of the file. If this option is not provided, the content
# will be read from disk.
# * `options.config` is configuration to pass along to the transform.
# * `done(err, result)` will be called with the transformed input.
#
exports.runTransform = (transform, file, options={}, done) ->
if !done?
done = options
options = {}
doTransform = (content) ->
data = ""
err = null
throughStream = if options.config?
transform(file, options.config)
else
transform(file)
throughStream.on "data", (d) ->
data += d
throughStream.on "end", ->
if !err then done null, data
throughStream.on "error", (e) ->
err = e
done err
throughStream.write content
throughStream.end()
if options.content
process.nextTick -> doTransform options.content
else
fs.readFile file, "utf-8", (err, content) ->
return done err if err
doTransform content