-
-
Notifications
You must be signed in to change notification settings - Fork 44
/
Gruntfile.dashboard.js
370 lines (330 loc) · 12.3 KB
/
Gruntfile.dashboard.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
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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
'use strict'
// ========================================
// A Gruntfile.js for chelonia-dashboard front-end
// ========================================
const path = require('path')
const chalk = require('chalk')
const crypto = require('crypto')
const { readFile } = require('fs/promises')
const { fork } = require('child_process')
const applyPortShift = (env) => {
// TODO: implement automatic port selection when `PORT_SHIFT` is 'auto'.
const API_PORT = 8000 + Number.parseInt(env.PORT_SHIFT || '0')
const API_URL = 'http://127.0.0.1:' + API_PORT
if (Number.isNaN(API_PORT) || API_PORT < 8000 || API_PORT > 65535) {
throw new RangeError(`Invalid API_PORT value: ${API_PORT}.`)
}
return { ...env, API_PORT, API_URL }
}
Object.assign(process.env, applyPortShift(process.env))
// paths
const dashboardRootDir = path.resolve(__dirname, 'backend/dashboard')
const resolvePathFromRoot = relPath => path.join(dashboardRootDir, relPath) // resolve a relative path against backend/dashboard folder
const distDir = path.resolve('dist-dashboard')
const distAssets = path.join(distDir, 'assets')
const distCSS = path.join(distDir, 'assets/css')
const distJS = path.join(distDir, 'assets/js')
const mainSrc = resolvePathFromRoot('main.js')
const mainScss = resolvePathFromRoot('assets/style/main.scss')
const backendIndex = 'backend/index.js'
const { NODE_ENV = 'development' } = process.env
const isDevelopment = NODE_ENV === 'development'
const isProduction = !isDevelopment
module.exports = (grunt) => {
require('load-grunt-tasks')(grunt)
const esbuildOptionsBag = {
default: {
bundle: true,
incremental: true,
loader: {
'.eot': 'file',
'.ttf': 'file',
'.woff': 'file',
'.woff2': 'file'
},
minifyIdentifiers: isProduction,
minifySyntax: isProduction,
minifyWhitespace: isProduction,
sourcemap: isDevelopment,
format: 'esm',
external: ['*.eot', '*.ttf', '*woff', '*.woff2'],
watch: false,
chunkNames: '[name]-[hash]-cached'
},
mainJS: {
// main.js -> dist/assets/js/main.js
entryPoints: [mainSrc],
outdir: distJS,
define: {
'process.env.NODE_ENV': `'${NODE_ENV}'`
},
splitting: !grunt.option('no-chunks')
},
mainCss: {
// assets/style/main.scss -> dist/assets/css/main.css
entryPoints: [mainScss],
outfile: path.join(distCSS, 'main.css')
}
}
// Used by both the alias plugin and the Vue plugin.
const aliasPluginOptions = {
entries: {
'@assets': './backend/dashboard/assets',
'@views': './backend/dashboard/views',
'@view-utils': './backend/dashboard/views/utils',
'@common': './backend/dashboard/common',
'@model': './backend/dashboard/model',
'@components': './backend/dashboard/views/components',
'@forms': './backend/dashboard/views/components/forms',
'@containers': './backend/dashboard/views/containers',
'@pages': './backend/dashboard/views/pages',
'@validators': './node_modules/vuelidate/dist/validators.min.js', // without this, the app gets the mistery 'process() is not defined' runtime error.
'vue': './node_modules/vue/dist/vue.esm.js', // without this, the app gets "[Vue warn]: You are using the runtime-only build of Vue" in the console.
'~': '.'
}
}
const browserSyncOptions = {
cors: true,
files: [
`${distJS}/main.js`,
`${distDir}/index.html`,
`${distAssets}/**/*`,
`${distCSS}/**/*`
],
logLevel: 'info',
open: false,
port: 3030,
proxy: {
target: process.env.API_URL,
ws: true
},
reloadDelay: 100,
reloadThrottle: 2000,
tunnel: grunt.option('tunnel') && `gi${crypto.randomBytes(2).toString('hex')}`
}
const puglintOptions = {}
const eslintOptions = {
format: 'stylish',
throwOnError: false,
throwOnWarning: false
}
const flowRemoveTypesPluginOptions = {
all: true,
cache: new Map()
}
const sassPluginOptions = {
cache: false,
sourceMap: isDevelopment,
outputStyle: isDevelopment ? 'expanded' : 'compressed',
loadPaths: [
resolvePathFromRoot('assets/style')
]
}
const vuePluginOptions = {
aliases: {
...aliasPluginOptions.entries
},
// This map's keys will be relative Vue file paths without leading dot,
// while its values will be corresponding compiled JS strings.
cache: new Map(),
debug: false,
flowtype: flowRemoveTypesPluginOptions
}
const stylelintOptions = {
cache: true,
config: require('./package.json').stylelint,
formatter: 'string',
syntax: 'scss',
throwOnError: false,
throwOnWarning: false
}
grunt.initConfig({
pkg: grunt.file.readJSON('./package.json'),
checkDependencies: { this: { options: { install: true } } },
exec: {
eslint: 'node ./node_modules/eslint/bin/eslint.js --cache "backend/dashboard/**/*.{js,vue}"',
puglint: '"./node_modules/.bin/pug-lint-vue" backend/dashboard/views',
stylelint: 'node ./node_modules/stylelint/bin/stylelint.js --cache "backend/dashboard/assets/style/**/*.{css,sass,scss}" "backend/dashboard/views/**/*.vue"',
strings: './strings.mac ./backend/dashboard --output ./backend/dashboard/assets/strings' // prep translation
},
clean: {
dist: [`${distDir}/*`],
strings: ['./strings/*']
},
copy: {
indexHtml: {
src: resolvePathFromRoot('index.html'),
dest: `${distDir}/index.html`
},
assets: {
cwd: resolvePathFromRoot('assets'),
src: [
'**/*',
'!style/**',
'!strings/*.strings' // don't need to copy files with .strings extention (only *.json files are used as translation tables in the app)
],
dest: distAssets,
expand: true
}
}
})
// -------------------------------------------------------------------------
// Grunt Tasks
// -------------------------------------------------------------------------
grunt.registerTask('default', ['dev-dashboard'])
let killKeepAlive = null
grunt.registerTask('keepalive', function () {
// This keeps grunt running after other async tasks have completed.
// eslint-disable-next-line no-unused-vars
killKeepAlive = this.async()
})
grunt.registerTask('dev-dashboard', [
'checkDependencies',
'build:watch',
'backend:relaunch',
'keepalive'
])
grunt.registerTask('build', function () {
const isDevBuild = this.flags.watch
grunt.task.run([
'clean:dist',
'exec:eslint',
!isDevBuild && 'exec:strings',
'exec:puglint',
'exec:stylelint',
'copy:indexHtml',
'copy:assets',
isDevBuild ? 'esbuild:watch' : 'esbuild'
].filter(Boolean))
})
let child = null
grunt.registerTask('backend:relaunch', '[internal]', function () {
const done = this.async() // Tell Grunt we're async.
const fork2 = function () {
grunt.log.writeln('backend: forking...')
child = fork(backendIndex, process.argv, {
env: {
...process.env,
IS_CHELONIA_DASHBOARD_DEV: true
},
execArgv: ['--require', '@babel/register']
})
child.on('error', (err) => {
if (err) {
console.error('error starting or sending message to child:', err)
process.exit(1)
}
})
child.on('exit', (c) => {
if (c !== 0) {
grunt.log.error(`child exited with error code: ${c}`.bold)
// ^C can cause c to be null, which is an OK error.
process.exit(c || 0)
}
})
done()
}
if (child) {
grunt.log.writeln('Killing child!')
// Wait for successful shutdown to avoid EADDRINUSE errors.
child.on('message', () => {
child = null
fork2()
})
child.send({ shutdown: 1 })
} else {
fork2()
}
})
grunt.registerTask('esbuild', async function () {
const done = this.async()
const { createEsbuildTask } = require('./scripts/esbuild-commands.js')
const aliasPlugin = require('./scripts/esbuild-plugins/alias-plugin.js')(aliasPluginOptions)
const flowRemoveTypesPlugin = require('./scripts/esbuild-plugins/flow-remove-types-plugin.js')(flowRemoveTypesPluginOptions)
const sassPlugin = require('esbuild-sass-plugin').sassPlugin(sassPluginOptions)
const vuePlugin = require('./scripts/esbuild-plugins/vue-plugin.js')(vuePluginOptions)
const buildMainJS = createEsbuildTask({
...esbuildOptionsBag.default,
...esbuildOptionsBag.mainJS,
plugins: [aliasPlugin, flowRemoveTypesPlugin, sassPlugin, vuePlugin]
})
// importing main.scss directly into .js or .vue file requires an additional post-build operation where
// we have to manually copy the main.css sitting next to main.js to assets/css folder. (refer to esbuildOtherOptionBags.main in Gruntfile.js)
// That works great but we can avoid this additional task by building & emitting main.css seperately.
const buildMainCss = createEsbuildTask({
...esbuildOptionsBag.default,
...esbuildOptionsBag.mainCss,
plugins: [aliasPlugin, sassPlugin]
})
try {
await buildMainJS.run()
await buildMainCss.run()
} catch (err) {
grunt.log.error(err.message)
process.exit(1)
}
if (!this.flags.watch) {
return done()
}
const browserSync = require('browser-sync').create('dashboard')
browserSync.init(browserSyncOptions)
const eslint = require('./scripts/esbuild-plugins/utils.js').createEslinter(eslintOptions)
const puglint = require('./scripts/esbuild-plugins/utils.js').createPuglinter(puglintOptions)
const stylelint = require('./scripts/esbuild-plugins/utils.js').createStylelinter(stylelintOptions)
const { chalkFileEvent, chalkLintingTime } = require('./scripts/esbuild-plugins/utils.js')
;[
[['backend/dashboard/index.html'], ['copy:indexHtml']],
[['backend/dashboard/assets/{fonts,images}/**/*'], ['copy:assets']],
// if file changes in dashboard/dist is watched, browser-sync gets into the infinite loop of reloading for some reason.
[['backend/dashboard/assets/style/**/*.scss'], [stylelint]],
[['backend/dashboard/**/*.js'], [eslint]],
[['backend/dashboard/views/**/*.vue'], [puglint, stylelint, eslint]]
].forEach(([globs, tasks]) => {
for (const glob of globs) {
browserSync.watch(glob, { ignoreInitial: true }, async (fileEventName, filePath) => {
const extension = path.extname(filePath)
grunt.log.writeln(chalkFileEvent(fileEventName, filePath))
if (['add', 'change'].includes(fileEventName)) {
const code = await readFile(filePath, 'utf8')
const linters = tasks.filter(task => typeof task === 'object')
const lintingStartMs = Date.now()
await Promise.all(linters.map(linter => linter.lintCode(code, filePath)))
// Don't crash the Grunt process on lint errors.
.catch(() => {})
// Log the linting time, formatted with Chalk.
grunt.log.writeln(chalkLintingTime(Date.now() - lintingStartMs, linters, [filePath]))
}
if (['change', 'unlink'].includes(fileEventName)) {
// Remove the corresponding plugin cache entry, if any.
if (extension === '.js') {
flowRemoveTypesPluginOptions.cache.delete(filePath)
} else if (extension === '.vue') {
vuePluginOptions.cache.delete(filePath)
}
// Clear the whole Vue plugin cache if a Sass or SVG file was
// changed since some compiled Vue files might include it.
if (['.sass', '.scss', '.svg'].includes(extension)) {
vuePluginOptions.cache.clear()
}
}
// Only rebuild the relevant entry point.
try {
if (['.scss', '.sass'].includes(extension)) {
buildMainCss.run({ fileEventName, filePath })
} else {
buildMainJS.run({ fileEventName, filePath })
}
} catch (err) {
grunt.log.error(err.message)
}
grunt.task.run(tasks.filter(task => typeof task === 'string'))
grunt.task.run(['keepalive'])
// Allow the task queue to move forward.
killKeepAlive && killKeepAlive()
})
}
})
grunt.log.writeln(chalk`{green browsersync:} setup done!`)
done()
})
}