This repository has been archived by the owner on Apr 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
index.js
211 lines (199 loc) · 6.53 KB
/
index.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
'use strict';
var _ = require('lodash');
var async = require('async');
var fs = require('graceful-fs');
var path = require('path');
var recast = require('recast');
var through2 = require('through2');
/**
* Extension to append to files to distinguish them from their original
* versions.
*/
var SUBEXTENSION = '.webkitassign';
/**
* Super-duper secret namespace for variables generated by this package.
*/
var NAMESPACE = '__webkitAssign__';
/**
* Returns `file` with a subextension (to avoid overwriting that file).
*/
var getOutputFile = function (file) {
var directory = path.dirname(file);
var base = path.basename(file);
var extension = path.extname(file);
var newName = base.substr(0, base.length - extension.length) + SUBEXTENSION + extension;
return path.join(directory, newName);
};
/**
* Traverse the SpiderMonkey AST node, calling `iteratee` on `node` and its
* children.
*/
var traverse = function traverse(node, iteratee) {
if (_.isArray(node)) {
_.forEach(node, function (child) {
traverse(child, iteratee);
});
} else if (_.isObject(node)) {
iteratee(node);
_.forOwn(node, function (child) {
traverse(child, iteratee);
});
}
};
var getNumberOfLines = function (string) {
return string.split(/\r\n|\r|\n/).length;
};
/**
* Currently the only directive is the Use Strict Directive (14.1.1).
*/
var DIRECTIVES = ['use strict'];
/**
* Replace all instances of dot property assignment with bracket notation
* assignment in `rawCodeString`. Return a transformed and formatted version of
* the code.
*/
var transformCode = function (rawCodeString) {
var ast = recast.parse(rawCodeString);
var propertyNames = [];
var protectProperty = function (node) {
var propertyName;
if (node.type === 'MemberExpression' && node.computed === false) {
propertyName = node.property.name;
propertyNames.push(propertyName);
// Use bracket notation for the assignment.
_.merge(node, {
computed: true,
property: {
type: 'Identifier',
name: NAMESPACE + propertyName
}
});
}
};
traverse(ast.program, function (node) {
if (node.type === 'AssignmentExpression') {
protectProperty(node.left);
} else if (node.type === 'UpdateExpression') {
protectProperty(node.argument);
}
});
if (propertyNames.length === 0) {
return rawCodeString;
}
propertyNames = _.uniq(propertyNames);
// Generate "enum" variable declarations for all the strings needed for
// bracket notation assignments.
var declarations = _.map(propertyNames, function (propertyName) {
var variableDeclarator = {
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: NAMESPACE + propertyName
},
init: {
type: 'Literal',
value: propertyName
}
};
return variableDeclarator;
});
var variableDeclaration = {
type: 'VariableDeclaration',
declarations: declarations,
kind: 'var'
};
// Find the index of the first non-directive to avoid functionally changing
// the code (by placing code before the directive prelude, which would
// negate any would-be directives).
var startIndex = 0;
var startNode;
_.forEach(ast.program.body, function (node, index) {
if (
node.type === 'ExpressionStatement' &&
node.expression.type === 'Literal' &&
_.includes(DIRECTIVES, node.expression.value)
) {
startIndex = index + 1;
startNode = node;
} else {
return false;
}
});
// Insert the variable declaration at that safe index.
ast.program.body.splice.apply(
ast.program.body,
[startIndex, 0].concat(variableDeclaration)
);
var printedCode = recast.print(ast).code;
// Remove the mysterious extraneous leading whitespace recast sometimes adds.
printedCode = printedCode.replace(/^\s+/, '');
// Detect when recast added a newline after or before the variable
// declaration.
var offendingLine, occurrence;
if (getNumberOfLines(printedCode) > getNumberOfLines(rawCodeString)) {
// Seek out the newline that was added. It might have been after the
// last directive if there were any, else the end of the variable
// declaration.
offendingLine = startNode ?
startNode.loc.end.line :
1;
occurrence = 0;
printedCode = printedCode.replace(/\r\n|\r|\n/g, function (match) {
occurrence += 1;
return occurrence === offendingLine ? '' : match;
});
}
return printedCode;
};
/**
* Return a stream that pipes in a whole file's contents and pipes out
* transformed code.
*/
var webkitAssignStream = function () {
var contents = '';
return through2(function (chunk, encoding, callback) {
// `encoding` is not used here.
/* jshint unused: true */
contents += chunk;
callback();
}, function (callback) {
var transformedCode = transformCode(contents);
this.push(transformedCode);
callback();
});
};
/**
* Create a transformed version of each file in `files`, where the transformed
* version of "script.js" is called "script.webkitassign.js". Invoke `callback`
* when done.
*/
var webkitAssignFiles = function (files, callback) {
async.each(files, function (file, callback) {
callback = _.once(callback);
var readStream = fs.createReadStream(file, {
encoding: 'utf8'
});
var transformStream = webkitAssignStream();
var outputFile = getOutputFile(file);
var writeStream = fs.createWriteStream(outputFile);
readStream
.on('error', callback)
.pipe(transformStream)
.on('error', callback)
.pipe(writeStream)
.on('error', callback)
.on('close', callback);
}, callback);
};
var webkitAssign = function (codeOrFiles, callback) {
if (_.isString(codeOrFiles)) {
var code = codeOrFiles;
return transformCode(code);
} else if (_.isArray(codeOrFiles)) {
var files = codeOrFiles;
webkitAssignFiles(files, callback);
} else if (arguments.length === 0) {
return webkitAssignStream();
}
};
module.exports = webkitAssign;