forked from bminer/module-concat
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
241 lines (227 loc) · 8.95 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
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
/* node-module-concat
Node.js module concatenation library
This library exposes a single function that concatenates Node.js modules
within a project. This can be used to obfuscate an entire project into a
single file. It can also be used to write client-side JavaScript code where
each file is written just like a Node.js module.
Known limitations:
- Dynamic `require()` statements don't work
(i.e. `require("./" + variable)`)
- `require.resolve` calls are not modified
- `require.cache` statements are not modified
*/
var fs = require("fs")
, path = require("path")
, stride = require("stride");
// Read main header/footer and header/footer for each file in the project
var header = fs.readFileSync(__dirname + "/lib/header.js").toString("utf8")
, footer = fs.readFileSync(__dirname + "/lib/footer.js").toString("utf8")
, fileHeader = fs.readFileSync(__dirname + "/lib/fileHeader.js").toString("utf8")
, fileFooter = fs.readFileSync(__dirname + "/lib/fileFooter.js").toString("utf8");
/* Concatenate all modules within a project.
The procedure works like this:
0.) A special header file is added to the `outputFile`. See
`./lib/header.js` for details.
1.) Read the entry module, as specified by its path.
2.) Scan the file for `require("...")` or `require('...')` statements.
Note: Dynamic require statements such as `require("./" + b)` are not
matched.
3.) If the fixed path specified by the `require("...")` statement is a
relative or absolute filesystem path (i.e. it begins with "./", "../",
or "/"), then that file is added to the project and recursively scanned
for `require` statements as in step #2. Additionally, the file is given
an unique identifier, and the `require("...")` statement is replaced
with `__require(id)` where `id` is the unique identifier. `__require`
is explained in more detail in `./lib/header.js`.
4.) In addition, any reference to `__dirname` or `__filename` is replaced
with `__getDirname(...)` and `__getFilename(...)`, which are explained
in `./lib/header.js`.
Note: __dirname and __filename references are replaced with paths
relative to the `outputFile` at the time of concatenation.
Therefore, if you move `outputFile` to a different path, the
__dirname and __filename reference will also change, but it will
still be the relative path at the time of concatenation. If you
don't know what I mean and you are having issues, please just read
./lib/header.js and look at the contents of the `outputFile`.
5.) Finally, the modified file is wrapped with a header and footer to
encapsulate the module within its own function. The wrapped code is
then written to the `outputFile`.
6.) Once all of the modules are written to the `outputFile`, a footer is
added to the `outputFile` telling Node to require and execute the entry
module.
Any source file added to the project has:
- A prepended header (./lib/fileHeader.js)
- An appended footer (./lib/fileFooter.js)
- Certain `require` statements replaced with `__require`
- All `__dirname` and `__filename` references replaced with
`__getDirname(...)` and `__getFilename(...)` references.
`modConcat(entryModule, outputFile, [options,] cb)`
- `entryModule` - the path to the entry point of the project to be
concatenated. This might be an `index.js` file, for example.
- `outputFile` - the path where the concatenated project file will be
written.
- `options` - Optional. An Object containing any of the following:
- `outputStreamOptions` - Options passed to `fs.createWriteStream` call
when the `outputFile` is opened for writing. Defaults to `null`.
- `excludeFiles` - An Array of files that should be excluded from the
project even if they were referenced by a `require(...)`.
Note: These `require` statements should probably be wrapped in a
try, catch block to prevent uncaught exceptions.
- `cb` - Callback of the form `cb(err, files)` where `files` is an Array
of files that have been included in the project.
*/
module.exports = function concat(entryModule, outputFile, opts, cb) {
// Reorganize arguments
if(typeof opts === "function") {
cb = opts;
opts = {};
}
opts = opts || {};
// Ensure that all paths have been resolved
if(opts.excludeFiles) {
for(var i = 0; i < opts.excludeFiles.length; i++) {
opts.excludeFiles[i] = path.resolve(opts.excludeFiles[i]);
}
}
// A list of all of the files read and included in the output thus far
var files = [];
// The file descriptor pointing to the `outputFile`
var fd;
stride(function writeMainHeader() {
var cb = this;
// Open WriteStream
var out = fs.createWriteStream(outputFile, opts.outputStreamOptions);
out.on("open", function(_fd) {
// Save the file descriptor
fd = _fd;
// Write main header
fs.write(fd, header, null, "utf8", cb);
});
}, function processEntryModule() {
// Add entry module to the project
files.push(path.resolve(entryModule) );
addToProject(fd, files[0], this);
}, function writeMainFooter() {
// Write main footer
fs.write(fd, footer, null, "utf8", this);
}).once("done", function(err) {
if(fd) {
fs.close(fd, function(closeErr) {
cb(err || closeErr, files);
});
} else {
cb(err, files);
}
});
function getPathRelativeToOutput(filePath) {
return path.relative(path.dirname(outputFile), filePath);
}
function addToProject(fd, filePath, cb) {
// Keep track of the current `files` length; we need it later
var lengthBefore = files.length;
stride(function writeHeader() {
// Write module header
fs.write(fd, fileHeader
.replace(/\$\{id\}/g, files.indexOf(filePath) )
.replace(/\$\{path\}/g, filePath), null, "utf8", this);
}, function writeExtraHeaderForJSON() {
// If this is a *.json file, add some extra fluff
if(path.extname(filePath) === ".json")
fs.write(fd, "module.exports = ", null, "utf8", this);
else
this(null);
}, function readFile() {
// Read file
fs.readFile(filePath, {"encoding": "utf8"}, this);
}, function processFile(code) {
// Scan file for `require(...)`, `__dirname`, and `__filename`
/* Quick notes about the somewhat intense `requireRegex`:
- require('...') and require("...") is matched
- The single or double quote matched is group 1
- Whitespace can go anywhere
- The module path matched is group 2
- Backslashes in the module path are escaped (i.e. for Windows paths)
*/
var requireRegex = /require\s*\(\s*(["'])((?:(?=(\\?))\3.)*)\1\s*\)/g,
dirnameRegex = /__dirname/g,
filenameRegex = /__filename/g;
code = code.replace(requireRegex, function(match, quote, modulePath) {
// Check to see if this require path begins with "./" or "../" or "/"
if(modulePath.match(/^\.?\.?\//) !== null) {
try {
modulePath = require.resolve(path.resolve(
path.join(path.dirname(filePath), modulePath)
) );
// Lookup this module's ID
var index = files.indexOf(modulePath);
if(index < 0) {
// Not found; add this module to the project
if(!opts.excludeFiles ||
opts.excludeFiles.indexOf(modulePath) < 0)
{
index = files.push(modulePath) - 1;
}
else {
// Ignore; do not replace
return match;
}
}
// Replace the `require` statement with `__require`
return "__require(" + index + ")";
} catch(e) {
// Ignore; do not replace
return match;
}
} else {
// Ignore; do not replace
return match;
}
})
// Replace `__dirname` with `__getDirname(...)`
.replace(dirnameRegex, "__getDirname(" +
JSON.stringify(getPathRelativeToOutput(filePath) ) + ")")
// Replace `__filename` with `__getFilename(...)`
.replace(filenameRegex, "__getFilename(" +
JSON.stringify(getPathRelativeToOutput(filePath) ) + ")");
// Write the modified code
fs.write(fd, code, null, "utf8", this);
}, function writeFooter() {
// Write module footer
index = files.indexOf(filePath);
fs.write(fd, fileFooter
.replace(/\$\{id\}/g, index)
.replace(/\$\{path\}/g, filePath), null, "utf8", this);
}, function addPendingFiles() {
// Process any pending files that were required by this file
var lengthAfter = files.length;
var args = [];
for(var i = lengthBefore; i < lengthAfter; i++) {
// Create new context for filename
(function(filename) {
args.push(function() {
addToProject(fd, filename, this);
});
})(files[i]);
}
// Pass the `args` Array to stride, and kick off the processing
stride.apply(null, args).once("done", this);
}).once("done", cb);
}
};
// If this module is invoked directly, behave like a cli
if(require.main === module) {
if(process.argv.length !== 4) {
console.log("Usage: node concat.js [entryModule] [outputFile]");
process.exit(1);
}
module.exports(process.argv[2], process.argv[3], function(err, files) {
if(err) {
console.error("Error", err.stack);
process.exit(1);
}
else {
console.log(files.length + " files written to " + process.argv[3] + ".");
console.log("Completed successfully.");
}
});
}