]> git.gir.st - VimFx.git/blob - gulpfile.coffee
VimFx v0.27.1
[VimFx.git] / gulpfile.coffee
1 fs = require('fs')
2 path = require('path')
3 gulp = require('gulp')
4 coffee = require('gulp-coffee')
5 coffeelint = require('gulp-coffeelint')
6 git = require('gulp-git')
7 header = require('gulp-header')
8 mustache = require('gulp-mustache')
9 preprocess = require('gulp-preprocess')
10 sloc = require('gulp-sloc')
11 tap = require('gulp-tap')
12 zip = require('gulp-zip')
13 marked = require('marked')
14 merge = require('merge2')
15 precompute = require('require-precompute')
16 request = require('request')
17 rimraf = require('rimraf')
18 pkg = require('./package.json')
19
20 DEST = 'build'
21 XPI = 'VimFx.xpi'
22 LOCALE = 'extension/locale'
23 TEST = 'extension/test'
24
25 BASE_LOCALE = 'en-US'
26 UPDATE_ALL = /\s*UPDATE_ALL$/
27
28 ADDON_PATH = 'chrome://vimfx'
29 BUILD_TIME = Date.now()
30
31 argv = process.argv.slice(2)
32
33 {join} = path
34 read = (filepath) -> fs.readFileSync(filepath).toString()
35 template = (data) -> mustache(data, {extension: ''})
36
37 gulp.task('clean', (callback) ->
38 rimraf(DEST, callback)
39 )
40
41 gulp.task('copy', ->
42 gulp.src(['extension/**/!(*.coffee|*.tmpl)', 'LICENSE', 'LICENSE-MIT'])
43 .pipe(gulp.dest(DEST))
44 )
45
46 gulp.task('node_modules', ->
47 dependencies = (name for name of pkg.dependencies)
48 # Note: When installing or updating node modules, make sure that the following
49 # glob does not include too much or too little!
50 gulp.src(
51 "node_modules/+(#{dependencies.join('|')})/\
52 {LICENSE*,{,**/!(test|examples)/}!(*min|*test*|*bench*).js}"
53 )
54 .pipe(gulp.dest("#{DEST}/node_modules"))
55 )
56
57 gulp.task('coffee', ->
58 test = '--test' in argv or '-t' in argv
59 gulp.src(
60 [
61 'extension/bootstrap.coffee'
62 'extension/lib/**/*.coffee'
63 ].concat(if test then 'extension/test/**/*.coffee' else []),
64 {base: 'extension'}
65 )
66 .pipe(preprocess({context: {
67 BUILD_TIME
68 ADDON_PATH: JSON.stringify(ADDON_PATH)
69 HOMEPAGE: JSON.stringify(pkg.homepage)
70 REQUIRE_DATA: JSON.stringify(precompute('.'), null, 2)
71 TESTS:
72 if test
73 JSON.stringify(fs.readdirSync(TEST)
74 .map((name) -> name.match(/^(test-.+)\.coffee$/)?[1])
75 .filter(Boolean)
76 )
77 else
78 null
79 }}))
80 .pipe(coffee({bare: true}))
81 .pipe(gulp.dest(DEST))
82 )
83
84 gulp.task('bootstrap-frame.js', ->
85 gulp.src('extension/bootstrap-frame.js.tmpl')
86 .pipe(mustache({ADDON_PATH}))
87 .pipe(tap((file) ->
88 file.path = file.path.replace(/\.js\.tmpl$/, "-#{BUILD_TIME}.js")
89 ))
90 .pipe(gulp.dest(DEST))
91 )
92
93 gulp.task('chrome.manifest', ->
94 gulp.src('extension/chrome.manifest.tmpl')
95 .pipe(template({locales: fs.readdirSync(LOCALE).map((locale) -> {locale})}))
96 .pipe(gulp.dest(DEST))
97 )
98
99 gulp.task('install.rdf', ->
100 [[{name: creator}], developers, contributors, translators] =
101 read('PEOPLE.md').trim().replace(/^#.+\n|^\s*-\s*/mg, '').split('\n\n')
102 .map((block) -> block.split('\n').map((name) -> {name}))
103
104 getDescription = (locale) -> read(join(LOCALE, locale, 'description')).trim()
105
106 descriptions = fs.readdirSync(LOCALE)
107 .filter((locale) -> locale != BASE_LOCALE)
108 .map((locale) -> {locale, description: getDescription(locale)})
109
110 gulp.src('extension/install.rdf.tmpl')
111 .pipe(template({
112 idSuffix: if '--unlisted' in argv or '-u' in argv then '-unlisted' else ''
113 version: pkg.version
114 homepage: pkg.homepage
115 minVersion: pkg.firefoxVersions.min
116 maxVersion: pkg.firefoxVersions.max
117 creator, developers, contributors, translators
118 defaultDescription: getDescription(BASE_LOCALE)
119 descriptions
120 }))
121 .pipe(gulp.dest(DEST))
122 )
123
124 gulp.task('templates', gulp.parallel(
125 'bootstrap-frame.js'
126 'chrome.manifest'
127 'install.rdf'
128 ))
129
130 gulp.task('build', gulp.series(
131 'clean',
132 gulp.parallel('copy', 'node_modules', 'coffee', 'templates')
133 ))
134
135 gulp.task('xpi-only', ->
136 gulp.src("#{DEST}/**/*")
137 .pipe(zip(XPI, {compress: false}))
138 .pipe(gulp.dest(DEST))
139 )
140
141 gulp.task('xpi', gulp.series('build', 'xpi-only'))
142
143 gulp.task('push-only', ->
144 body = fs.readFileSync(join(DEST, XPI))
145 request.post({url: 'http://localhost:8888', body})
146 )
147
148 gulp.task('push', gulp.series('xpi', 'push-only'))
149
150 gulp.task('default', gulp.series('push'))
151
152 # coffeelint-forbidden-keywords has `require('coffee-script/register');` in its
153 # index.js :(
154 gulp.task('lint-workaround', ->
155 gulp.src('node_modules/coffeescript/')
156 .pipe(gulp.symlink('node_modules/coffee-script'))
157 )
158
159 gulp.task('lint-only', ->
160 gulp.src(['extension/**/*.coffee', 'gulpfile.coffee'])
161 .pipe(coffeelint())
162 .pipe(coffeelint.reporter())
163 .pipe(coffeelint.reporter('fail'))
164 )
165
166 gulp.task('lint', gulp.series('lint-workaround', 'lint-only'))
167
168 gulp.task('sloc', ->
169 gulp.src([
170 'extension/bootstrap.coffee'
171 'extension/lib/!(migrations|legacy).coffee'
172 ])
173 .pipe(sloc())
174 )
175
176 gulp.task('release', (callback) ->
177 {version} = pkg
178 message = "VimFx v#{version}"
179 today = new Date().toISOString()[...10]
180 merge([
181 gulp.src('package.json')
182 gulp.src('CHANGELOG.md')
183 .pipe(header("### #{version} (#{today})\n\n"))
184 .pipe(gulp.dest('.'))
185 ])
186 .pipe(git.commit(message))
187 .on('end', ->
188 git.tag("v#{version}", message, callback)
189 )
190 return
191 )
192
193 gulp.task('changelog', (callback) ->
194 num = 1
195 for arg in argv when /^-[1-9]$/.test(arg)
196 num = Number(arg[1])
197 entries = read('CHANGELOG.md').split(/^### .+/m)[1..num].join('')
198 process.stdout.write(html(entries))
199 callback()
200 )
201
202 gulp.task('readme', (callback) ->
203 process.stdout.write(html(read('README.md')))
204 callback()
205 )
206
207 # Reduce markdown to the small subset of HTML that AMO allows. Note that AMO
208 # converts newlines to `<br>`.
209 html = (string) ->
210 return marked(string)
211 .replace(/// <h\d [^>]*> ([^<>]+) </h\d> ///g, '\n\n<b>$1</b>')
212 .replace(///\s* <p> ((?: [^<] | <(?!/p>) )+) </p>///g, (match, text) ->
213 return "\n#{text.replace(/\s*\n\s*/g, ' ')}\n\n"
214 )
215 .replace(///<li> ((?: [^<] | <(?!/li>) )+) </li>///g, (match, text) ->
216 return "<li>#{text.replace(/\s*\n\s*/g, ' ')}</li>"
217 )
218 .replace(/<br>/g, '\n')
219 .replace(///<(/?)kbd>///g, '<$1code>')
220 .replace(/<img[^>]*>\s*/g, '')
221 .replace(/\n\s*\n/g, '\n\n')
222 .trim() + '\n'
223
224 gulp.task('faster', ->
225 gulp.src('gulpfile.coffee')
226 .pipe(coffee({bare: true}))
227 .pipe(gulp.dest('.'))
228 )
229
230 gulp.task('sync-locales', (callback) ->
231 baseLocale = BASE_LOCALE
232 compareLocale = null
233 for arg in argv when arg[...2] == '--'
234 name = arg[2..]
235 if name[-1..] == '?' then compareLocale = name[...-1] else baseLocale = name
236
237 results = fs.readdirSync(join(LOCALE, baseLocale))
238 .filter((file) -> path.extname(file) == '.properties')
239 .map(syncLocale.bind(null, baseLocale))
240
241 if baseLocale == BASE_LOCALE
242 report = []
243 for {fileName, untranslated, total} in results
244 report.push("#{fileName}:")
245 for localeName, strings of untranslated
246 paddedName = "#{localeName}: "[...6]
247 percentage = Math.round((1 - strings.length / total) * 100)
248 if localeName == compareLocale or compareLocale == null
249 report.push(" #{paddedName} #{percentage}%")
250 if localeName == compareLocale
251 report.push(strings.map((string) -> " #{string}")...)
252 process.stdout.write(report.join('\n') + '\n')
253
254 callback()
255 )
256
257 syncLocale = (baseLocaleName, fileName) ->
258 basePath = join(LOCALE, baseLocaleName, fileName)
259 base = parseLocaleFile(read(basePath))
260 untranslated = {}
261 for localeName in fs.readdirSync(LOCALE)
262 localePath = join(LOCALE, localeName, fileName)
263 locale = parseLocaleFile(read(localePath))
264 untranslated[localeName] = []
265 newLocale = base.template.map((line, index) ->
266 if Array.isArray(line)
267 [key] = line
268 baseValue = base.keys[key]
269 value =
270 if UPDATE_ALL.test(baseValue) or key not of locale.keys
271 baseValue.replace(UPDATE_ALL, '')
272 else
273 locale.keys[key]
274 result = "#{key}=#{value}"
275 if value == base.keys[key] and value != ''
276 untranslated[localeName].push("#{index + 1}: #{result}")
277 return result
278 else
279 return line
280 )
281 fs.writeFileSync(localePath, newLocale.join(base.newline))
282 delete untranslated[baseLocaleName]
283 return {fileName, untranslated, total: Object.keys(base.keys).length}
284
285 parseLocaleFile = (fileContents) ->
286 keys = {}
287 lines = []
288 [newline] = fileContents.match(/\r?\n/)
289 for line in fileContents.split(newline)
290 line = line.trim()
291 [match, key, value] = line.match(///^ ([^=]+) = (.*) $///) ? []
292 if match
293 keys[key] = value
294 lines.push([key])
295 else
296 lines.push(line)
297 return {keys, template: lines, newline}
298
299 generateHTMLTask = (filename, message) ->
300 gulp.task(filename, (callback) ->
301 unless fs.existsSync(filename)
302 process.stdout.write(message(filename))
303 callback()
304 return
305 gulp.src(filename)
306 .pipe(tap((file) ->
307 file.contents = new Buffer(generateTestHTML(file.contents.toString()))
308 ))
309 .pipe(gulp.dest('.'))
310 )
311
312 generateHTMLTask('help.html', (filename) -> """
313 First enable the “Copy to clipboard” line in help.coffee, show the help
314 dialog and finally dump the clipboard into #{filename}.
315 """)
316
317 generateHTMLTask('hints.html', (filename) -> """
318 First enable the “Copy to clipboard” line in modes.coffee, show the
319 hint markers, activate the “Increase count” command and finally dump the
320 clipboard into #{filename}.
321 """)
322
323 testHTMLPrelude = '''
324 <!doctype html>
325 <meta charset=utf-8>
326 <title>VimFx test</title>
327 <style>
328 * {margin: 0;}
329 body > :first-child {min-height: 100vh; width: 100vw;}
330 </style>
331 <link rel=stylesheet href=extension/skin/style.css>
332 '''
333
334 generateTestHTML = (dumpedHTML) ->
335 return testHTMLPrelude + dumpedHTML
336 .replace(/^<\w+ xmlns="[^"]+"/, '<div')
337 .replace(/\w+>$/, 'div>')
338 .replace(/<(\w+)([^>]*)\/>/g, '<$1$2></$1>')
Imprint / Impressum