Completed
Push — master ( 5dc4b7...3c0927 )
by Marcelo
11s
created

src/build.js   A

Complexity

Total Complexity 35
Complexity/F 1.17

Size

Lines of Code 289
Function Count 30

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 1 Features 0
Metric Value
cc 0
wmc 35
c 8
b 1
f 0
nc 1
mnd 1
bc 17
fnc 30
dl 0
loc 289
rs 9
bpm 0.5666
cpm 1.1666
noi 7

13 Functions

Rating   Name   Duplication   Size   Complexity  
A build.js ➔ resolveOutputTarget 0 10 1
A build.js ➔ listFiles 0 5 1
A build.js ➔ filterFiles 0 20 4
A build.js ➔ linkFiles 0 5 1
A build.js ➔ precompile 0 8 1
A build.js ➔ linkAutoComplete 0 6 1
A build.js ➔ localesToPairs 0 5 1
A build.js ➔ linkLocales 0 8 1
A build.js ➔ createMetaFile 0 3 1
A build.js ➔ getProjectName 0 5 1
A build.js ➔ ??? 0 7 1
A build.js ➔ addToZip 0 3 1
A build.js ➔ build 0 12 1
1
import path from 'path';
2
import Zip from 'jszip';
3
import Promise, { all, promisifyAll, resolve } from 'bluebird';
4
import {
5
    complement,
6
    concat,
7
    contains,
8
    curry,
9
    drop,
10
    equals,
11
    endsWith,
12
    filter,
13
    head,
14
    identity,
15
    ifElse,
16
    is,
17
    join,
18
    lensProp,
19
    map,
20
    mapObjIndexed,
21
    merge,
22
    over,
23
    propEq,
24
    replace,
25
    sort,
26
    startsWith,
27
    subtract,
28
    takeWhile,
29
    test,
30
    tryCatch,
31
    unary,
32
    union,
33
    without
34
} from 'ramda';
35
import deepmerge from 'deepmerge';
36
import { emitSuccess, emitWarning } from './input';
37
import { getProperties } from './vm';
38
import { compileModulesFromSource, ensureNoImports, inspect } from './module';
39
40
const fs = promisifyAll(require('fs'));
41
42
const defaultFileOptions = { date: new Date(1149562800000) };
43
const requiredFiles = ['package.json', 'index.js'];
44
45
const localeByFile = drop(8)
46
    & takeWhile(complement(equals('.')))
47
    & join('');
48
49
/**
50
 * Converts a list of locale files to pairs containing locale string and content
51
 *
52
 * @param {String[]} localeFiles
53
 * @return {Promise}
54
 */
55
function localesToPairs(localeFiles) {
56
    return all(localeFiles.map(localeFile => fs.readFileAsync(localeFile, 'utf-8')
57
        .then(JSON.parse)
58
        .then(json => [localeByFile(localeFile), json])));
59
}
60
61
/**
62
 * Projects locale for each translatable subfield
63
 *
64
 * @param {String} locale
65
 * @param {Object} config
66
 * @return {Object}
67
 */
68
const project = curry((locale, config) => ({
69
    title: { [locale]: config.title },
70
    description: { [locale]: config.description },
71
    preview: { [locale]: config.preview },
72
    params: mapObjIndexed(param => merge(param,
73
        { description: { [locale]: param.description } }), config.params)
74
}));
75
76
/**
77
 * Lazily runs the extension using all possible listed locales and extracts
78
 * the meta-data.
79
 *
80
 * @param {String} source
81
 * @param {[(String, *)]} locales
82
 * @return {Promise}
83
 */
84
const runInAllLocales = curry((source, locales) =>
85
    compileModulesFromSource(source).then(modules =>
86
        all([['default', {}], ...locales].map(([locale, strings]) =>
87
            getProperties({ name: `precompile-${locale}`, source }, strings, modules)
88
                .then(project(locale))))
89
                .then(ifElse(propEq('length', 1), head, unary(deepmerge.all)))));
90
91
/**
92
 * Creates a meta file where the information about precompilation is stored
93
 *
94
 * @param {Object} locales
95
 * @return {Promise}
96
 */
97
function createMetaFile(locales) {
98
    return fs.writeFileAsync('.meta', JSON.stringify(locales));
99
}
100
101
/**
102
 * Precompiles linked files, generating a .meta file with all the meta data
103
 *
104
 * @param {Object<String, String[]>} { code, files }
0 ignored issues
show
Documentation introduced by
The parameter { does not exist. Did you maybe forget to remove this comment?
Loading history...
105
 * @return {Promise}
106
 */
107
function precompile({ code, files }) {
108
    return resolve(files)
109
        .then(filter(test(/^locales(\/|\\)[a-z]{2,3}(_[A-Z]{2})?\.json$/)))
110
        .then(localesToPairs)
111
        .then(runInAllLocales(code))
112
        .then(createMetaFile)
113
        .thenReturn(['.meta', ...files]);
114
}
115
116
/**
117
 * Ensures there are missing no files in order to a allow a basic compilation
118
 * and filter the used modules. It also warns about possible improvements in the
119
 * extensions
120
 *
121
 * @param {String[]} files
122
 * @return {Promise}
123
 */
124
function filterFiles(files) {
125
    const clearModule = replace(/^\.\//, '');
126
    const missingFiles = without(files, requiredFiles);
127
    const hasIcon = contains('icon.png', files);
128
    const resources = hasIcon ? ['icon.png'] : [];
129
130
    if (missingFiles.length > 0) {
131
        throw new Error(`missing ${missingFiles.join(', ')} from the project`);
132
    }
133
134
    if (!hasIcon) {
135
        emitWarning('compiling extension without providing an icon.png file');
136
    }
137
138
    return fs.readFileAsync('index.js', 'utf-8')
139
        .then(inspect)
140
        .then(over(lensProp('modules'), filter(startsWith('./'))))
141
        .then(({ code, modules }) => ({
142
            code, files: union(modules.map(clearModule), concat(resources, requiredFiles)) }));
143
}
144
145
/**
146
 * Returns all the files in a directory if it exists. Otherwise, return an
147
 * empty array as fallback (everything inside a promise)
148
 *
149
 * @param {String} directory
150
 * @return {String[]}
151
 */
152
function listFiles(directory) {
153
    return fs.lstatAsync(directory)
154
        .then(lstat => lstat.isDirectory() ? fs.readdirAsync(directory) : [])
155
        .catchReturn([]);
156
}
157
158
/**
159
 * Links autocomplete files
160
 *
161
 * @return {Promise}
162
 */
163
function linkAutoComplete() {
164
    return listFiles('autocomplete')
165
        .then(filter(endsWith('.js')) & map(path.join('autocomplete', _)))
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
166
        .tap(files => all(files.map(file => fs.readFileAsync(file)
167
            .then(ensureNoImports(file)))));
168
}
169
170
/**
171
 * Links locale files
172
 *
173
 * @return {Promise}
174
 */
175
function linkLocales() {
176
    return listFiles('locales')
177
        .then(filter(test(/^[a-z]{2}(_[A-Z]{2,3})?\.json$/)) & map(path.join('locales', _)))
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
178
        .filter(location => fs.readFileAsync(location)
179
            .then(JSON.parse & is(Object))
180
            .catchReturn(false))
181
        .catchReturn([]);
182
}
183
184
/**
185
 * Links the files to precompilation, including locales and autocomplete
186
 * scripts. For autocomplete files, ensuring it is a valid script without
187
 * requires. For locales, filtering true locale files and appending the full
188
 * qualified name for current files.
189
 *
190
 * @param {Object<String, String[]>} { code, files }
0 ignored issues
show
Documentation introduced by
The parameter { does not exist. Did you maybe forget to remove this comment?
Loading history...
191
 * @return {Promise}
192
 */
193
function linkFiles({ code, files }) {
194
    return all([linkLocales(), linkAutoComplete()])
195
        .spread(union)
196
        .then(union(files) & sort(subtract) & (files => ({ code, files })));
197
}
198
199
/**
200
 * Opens package.json and extrats its contents. Returns a promise containing
201
 * the file list to be zipped and the package.json content parsed
202
 *
203
 * @param {String} dir
204
 * @return {Promise}
205
 */
206
function getProjectName(dir) {
207
    return fs.readFileAsync(path.join(dir, 'package.json'))
208
        .then(JSON.parse & _.name)
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
209
        .catchThrow(new Error('Failed to parse package.json from the project'));
210
}
211
212
/**
213
 * Generates a zip package using a node buffer containing the necessary files
214
 *
215
 * @param {String} dir
216
 * @param {String[]} files
217
 * @param {String} name
0 ignored issues
show
Documentation introduced by
The parameter name does not exist. Did you maybe forget to remove this comment?
Loading history...
218
 */
219
const createZip = curry((dir, files) => {
220
    const zip = new Zip();
221
    files.forEach(addToZip(zip, dir, _));
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
222
    return zip;
223
});
224
225
/**
226
 * Taking account the -o parameter can be used to specify the output directory,
227
 * let's deal with it
228
 *
229
 * @param {String} customPath
230
 * @param {String} filename
231
 * @return {String}
232
 */
233
function resolveOutputTarget(customPath, filename) {
234
    const realPath = path.resolve('.', customPath);
235
236
    const getPath = tryCatch(realPath => fs.lstatSync(realPath).isDirectory()
237
        ? path.join(realPath, filename)
238
        : realPath
239
    , identity);
240
241
    return getPath(realPath);
242
}
243
244
/**
245
 * Saves the zip file from buffer to the filesystem
246
 *
247
 * @param {String} dir
248
 * @param {Zip} zip
249
 * @param {String} name
250
 */
251
const saveZip = curry((dir, zip, name) => {
252
    const target = resolveOutputTarget(dir, `${name}.rung`);
253
254
    return new Promise((resolve, reject) => {
255
        zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true })
256
            .pipe(fs.createWriteStream(target))
257
            .on('error', reject)
258
            .on('finish', ~resolve(target));
259
    });
260
});
261
262
/**
263
 * Appends a file or folder to the zip buffer
264
 *
265
 * @param {Zip} zip
266
 * @param {String} dir
267
 * @param {String} filename
268
 */
269
function addToZip(zip, dir, filename) {
270
    return zip.file(filename, fs.readFileSync(path.join(dir, filename)), defaultFileOptions);
271
}
272
273
/**
274
 * Precompiles an extension and generates a .rung package
275
 *
276
 * @param {Object} args
277
 */
278
export default function build(args) {
279
    const dir = path.resolve('.', args._[1] || '');
280
281
    return fs.readdirAsync(dir)
282
        .then(filterFiles)
283
        .then(linkFiles)
284
        .then(precompile)
285
        .then(createZip(dir))
286
        .then(zip => all([zip, getProjectName(dir)]))
287
        .spread(saveZip(args.output || '.'))
288
        .tap(~emitSuccess('Rung extension compilation'));
289
}
290