547 行
16 KiB
JavaScript
547 行
16 KiB
JavaScript
/*!
|
|
* Grunt file
|
|
*
|
|
* @package VisualEditor
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* Grunt configuration
|
|
*
|
|
* @param {Object} grunt The grunt object
|
|
*/
|
|
module.exports = function ( grunt ) {
|
|
const modules = grunt.file.readJSON( 'build/modules.json' ),
|
|
moduleUtils = require( './build/moduleUtils' ),
|
|
fg = require( 'fast-glob' ),
|
|
rebaserBuildFiles = moduleUtils.makeBuildList( modules, [ 'rebaser.build' ] ),
|
|
veRebaseFiles = moduleUtils.makeBuildList( modules, [ 'visualEditor.rebase.build' ] ),
|
|
coreBuildFiles = moduleUtils.makeBuildList( modules, [ 'visualEditor.build' ] ),
|
|
coreBuildFilesApex = moduleUtils.makeBuildList( modules, [ 'visualEditor.build.apex' ] ),
|
|
coreBuildFilesWikimediaUI = moduleUtils.makeBuildList( modules, [ 'visualEditor.build.wikimediaui' ] ),
|
|
testFiles = moduleUtils.makeBuildList( modules, [ 'visualEditor.test' ] ).scripts,
|
|
demoPages = ( function () {
|
|
const files = grunt.file.expand( 'demos/ve/pages/*.html' );
|
|
return files.map( ( file ) => file.match( /^.*pages\/(.+).html$/ )[ 1 ] );
|
|
}() );
|
|
|
|
const distLessFiles = {
|
|
'dist/visualEditor-apex.css': 'dist/visualEditor-apex.less',
|
|
'dist/visualEditor-wikimediaui.css': 'dist/visualEditor-wikimediaui.less',
|
|
'dist/visualEditor-rebase.css': 'dist/visualEditor-rebase.less'
|
|
};
|
|
|
|
grunt.loadNpmTasks( 'grunt-banana-checker' );
|
|
grunt.loadNpmTasks( 'grunt-contrib-clean' );
|
|
grunt.loadNpmTasks( 'grunt-contrib-concat' );
|
|
grunt.loadNpmTasks( 'grunt-contrib-copy' );
|
|
grunt.loadNpmTasks( 'grunt-contrib-less' );
|
|
grunt.loadNpmTasks( 'grunt-contrib-watch' );
|
|
grunt.loadNpmTasks( 'grunt-css-url-embed' );
|
|
grunt.loadNpmTasks( 'grunt-cssjanus' );
|
|
grunt.loadNpmTasks( 'grunt-eslint' );
|
|
grunt.loadNpmTasks( 'grunt-karma' );
|
|
grunt.loadNpmTasks( 'grunt-stylelint' );
|
|
grunt.loadNpmTasks( 'grunt-tyops' );
|
|
grunt.loadTasks( 'build/tasks' );
|
|
|
|
// We want to use `grunt watch` to start this and karma watch together.
|
|
grunt.renameTask( 'watch', 'runwatch' );
|
|
|
|
/**
|
|
* Build an object of required coverage percentages
|
|
*
|
|
* @param {number} pc Percentage coverage required (for all aspects)
|
|
* @return {Object} required coverage percentages
|
|
*/
|
|
function coverAll( pc ) {
|
|
return {
|
|
functions: pc,
|
|
branches: pc,
|
|
statements: pc,
|
|
lines: pc
|
|
};
|
|
}
|
|
|
|
grunt.initConfig( {
|
|
pkg: grunt.file.readJSON( 'package.json' ),
|
|
clean: {
|
|
dist: [ 'dist/*', 'coverage/*' ],
|
|
less: Object.values( distLessFiles )
|
|
},
|
|
concat: {
|
|
'rebaser.build': {
|
|
options: {
|
|
banner: grunt.file.read( 'build/rebaser-banner.txt' ),
|
|
footer: grunt.file.read( 'build/rebaser-footer.txt' )
|
|
},
|
|
dest: 'dist/ve-rebaser.js',
|
|
src: rebaserBuildFiles.scripts
|
|
},
|
|
'visualEditor.rebase.scripts': {
|
|
options: {
|
|
banner: grunt.file.read( 'build/banner.txt' ),
|
|
sourceMap: true
|
|
},
|
|
dest: 'dist/visualEditor-rebase.js',
|
|
src: veRebaseFiles.scripts
|
|
},
|
|
'visualEditor.rebase.styles': {
|
|
options: {
|
|
banner: grunt.file.read( 'build/banner.txt' )
|
|
},
|
|
dest: 'dist/visualEditor-rebase.less',
|
|
src: veRebaseFiles.styles
|
|
},
|
|
js: {
|
|
options: {
|
|
banner: grunt.file.read( 'build/banner.txt' ),
|
|
sourceMap: true
|
|
},
|
|
dest: 'dist/visualEditor.js',
|
|
src: coreBuildFiles.scripts
|
|
},
|
|
'css-apex': {
|
|
options: {
|
|
banner: grunt.file.read( 'build/banner.txt' )
|
|
},
|
|
dest: 'dist/visualEditor-apex.less',
|
|
src: coreBuildFilesApex.styles
|
|
},
|
|
'css-wikimediaui': {
|
|
options: {
|
|
banner: grunt.file.read( 'build/banner.txt' )
|
|
},
|
|
dest: 'dist/visualEditor-wikimediaui.less',
|
|
src: coreBuildFilesWikimediaUI.styles
|
|
},
|
|
// HACK: Ideally these libraries would provide their own distribution files (T95667)
|
|
'jquery.i18n': {
|
|
dest: 'dist/lib/jquery.i18n.js',
|
|
src: modules[ 'jquery.i18n' ].scripts
|
|
},
|
|
'jquery.uls.data': {
|
|
dest: 'dist/lib/jquery.uls.data.js',
|
|
src: modules[ 'jquery.uls.data' ].scripts
|
|
}
|
|
},
|
|
less: {
|
|
options: {
|
|
// Throw errors if we try to calculate mixed units, like `px` and `em` values.
|
|
strictUnits: true,
|
|
// Force LESS v3.0.0+ to let us use mixins before we later upgrade to @plugin
|
|
// architecture.
|
|
javascriptEnabled: true
|
|
},
|
|
dist: {
|
|
files: distLessFiles
|
|
}
|
|
},
|
|
cssjanus: {
|
|
apex: {
|
|
dest: 'dist/visualEditor-apex.rtl.css',
|
|
src: 'dist/visualEditor-apex.css'
|
|
},
|
|
wikimediaui: {
|
|
dest: 'dist/visualEditor-wikimediaui.rtl.css',
|
|
src: 'dist/visualEditor-wikimediaui.css'
|
|
}
|
|
},
|
|
cssUrlEmbed: {
|
|
options: {
|
|
// TODO: Image paths are relative to their folders, but the files have already been
|
|
// flattened as this point, so supporting more that one baseDir is not possible.
|
|
baseDir: 'src/ui/styles/nodes'
|
|
},
|
|
dist: {
|
|
files: {
|
|
'dist/visualEditor-apex.css': 'dist/visualEditor-apex.css',
|
|
'dist/visualEditor-apex.rtl.css': 'dist/visualEditor-apex.rtl.css',
|
|
'dist/visualEditor-wikimediaui.css': 'dist/visualEditor-wikimediaui.css',
|
|
'dist/visualEditor-wikimediaui.rtl.css': 'dist/visualEditor-wikimediaui.rtl.css'
|
|
}
|
|
}
|
|
},
|
|
copy: {
|
|
i18n: {
|
|
src: 'i18n/*.json',
|
|
dest: 'dist/',
|
|
expand: true
|
|
},
|
|
lib: {
|
|
src: [ 'lib/**', '!lib/jquery.i18n/**', '!lib/jquery.uls/**' ],
|
|
dest: 'dist/',
|
|
expand: true
|
|
}
|
|
},
|
|
buildloader: {
|
|
desktopDemoApex: {
|
|
targetFile: 'demos/ve/desktop.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.desktop.standalone.apex',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.desktop.standalone.apex.demo' ],
|
|
env: {
|
|
debug: true
|
|
},
|
|
pathPrefix: '../../',
|
|
i18n: [ 'i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
desktopDemoApexDist: {
|
|
targetFile: 'demos/ve/desktop-dist.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.desktop.standalone.apex.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.desktop.standalone.apex.demo' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
desktopDemoWikimediaUI: {
|
|
targetFile: 'demos/ve/desktop-wikimediaui.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.desktop.standalone.wikimediaui',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.desktop.standalone.wikimediaui.demo' ],
|
|
env: {
|
|
debug: true
|
|
},
|
|
pathPrefix: '../../',
|
|
i18n: [ 'i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
desktopDemoWikimediaUIDist: {
|
|
targetFile: 'demos/ve/desktop-dist-wikimediaui.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.desktop.standalone.wikimediaui.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.desktop.standalone.wikimediaui.demo' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
mobileDemo: {
|
|
targetFile: 'demos/ve/mobile.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.mobile.standalone',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.mobile.standalone.demo' ],
|
|
env: {
|
|
debug: true
|
|
},
|
|
pathPrefix: '../../',
|
|
i18n: [ 'i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
mobileDemoDist: {
|
|
targetFile: 'demos/ve/mobile-dist.html',
|
|
template: 'demos/ve/demo.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.mobile.standalone.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.mobile.standalone.demo' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
demoPages: demoPages
|
|
},
|
|
minimalDemo: {
|
|
targetFile: 'demos/ve/minimal.html',
|
|
template: 'demos/ve/minimal.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.standalone.apex.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.minimal.standalone.demo' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
dir: 'ltr',
|
|
langList: false
|
|
},
|
|
minimalDemoRtl: {
|
|
targetFile: 'demos/ve/minimal-rtl.html',
|
|
template: 'demos/ve/minimal.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.standalone.apex.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.minimal.standalone.demo' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
dir: 'rtl',
|
|
langList: false
|
|
},
|
|
performanceTest: {
|
|
targetFile: 'demos/ve/performance.html',
|
|
template: 'demos/ve/performance.html.template',
|
|
modules: modules,
|
|
load: [
|
|
'visualEditor.standalone.apex.dist',
|
|
'visualEditor.standalone.read'
|
|
],
|
|
run: [ 'visualEditor.test.performance' ],
|
|
pathPrefix: '../../',
|
|
i18n: [ 'dist/i18n/', 'lib/oojs-ui/i18n/' ],
|
|
indent: '\t\t',
|
|
dir: 'ltr',
|
|
langList: false
|
|
},
|
|
test: {
|
|
targetFile: 'tests/index.html',
|
|
template: 'tests/index.html.template',
|
|
modules: modules,
|
|
env: {
|
|
test: true
|
|
},
|
|
load: [ 'visualEditor.test' ],
|
|
pathPrefix: '../',
|
|
indent: '\t\t'
|
|
}
|
|
},
|
|
tyops: {
|
|
options: {
|
|
typos: 'build/typos.json'
|
|
},
|
|
src: fg.globSync( [
|
|
'**/*.{js,json,less,css,txt,md,sh}',
|
|
'!**/package-lock.json',
|
|
'!build/typos.json',
|
|
'!lib/**',
|
|
'!**/i18n/**/*.json',
|
|
'!**/{coverage,dist,docs,node_modules}/**',
|
|
'!.git/**'
|
|
] )
|
|
// Overwrite ignores
|
|
.concat( fg.globSync( [
|
|
'**/i18n/**/en.json',
|
|
'**/i18n/**/qqq.json',
|
|
'!**/{coverage,dist,docs,node_modules}/**'
|
|
] ) )
|
|
},
|
|
eslint: {
|
|
options: {
|
|
cache: true,
|
|
fix: grunt.option( 'fix' )
|
|
},
|
|
all: fg.globSync( [
|
|
'**/*.{js,json}',
|
|
'demos/**/*.html',
|
|
'!lib/**',
|
|
'!**/{coverage,dist,docs,node_modules}/**'
|
|
] )
|
|
},
|
|
stylelint: {
|
|
options: {
|
|
reportNeedlessDisables: true
|
|
},
|
|
all: fg.globSync( [
|
|
'**/*.css',
|
|
'!lib/**',
|
|
'!**/{coverage,dist,docs,node_modules}/**'
|
|
] )
|
|
},
|
|
banana: {
|
|
all: 'i18n/'
|
|
},
|
|
karma: {
|
|
options: {
|
|
files: testFiles,
|
|
frameworks: [ 'qunit' ],
|
|
reporters: [ 'dots' ],
|
|
singleRun: true,
|
|
browserDisconnectTimeout: 5000,
|
|
browserDisconnectTolerance: 2,
|
|
browserNoActivityTimeout: 30000,
|
|
customLaunchers: {
|
|
ChromeCustom: {
|
|
base: 'ChromeHeadless',
|
|
// Chrome requires --no-sandbox in Docker/CI.
|
|
flags: process.env.CHROMIUM_FLAGS ?
|
|
process.env.CHROMIUM_FLAGS.split( ' ' ) :
|
|
undefined
|
|
}
|
|
},
|
|
autoWatch: false
|
|
},
|
|
chrome: {
|
|
browsers: [ 'ChromeCustom' ],
|
|
preprocessors: {
|
|
'rebaser/src/**/*.js': [ 'coverage' ],
|
|
'src/**/*.js': [ 'coverage' ]
|
|
},
|
|
reporters: [ 'mocha', 'coverage' ],
|
|
coverageReporter: {
|
|
dir: 'coverage/',
|
|
subdir: '.',
|
|
reporters: [
|
|
{ type: 'clover' },
|
|
{ type: 'html' },
|
|
{ type: 'text-summary' }
|
|
],
|
|
// https://github.com/karma-runner/karma-coverage/blob/v1.1.2/docs/configuration.md#check
|
|
check: {
|
|
global: coverAll( 60 ),
|
|
each: {
|
|
functions: 20,
|
|
branches: 20,
|
|
statements: 20,
|
|
lines: 20,
|
|
excludes: [
|
|
'rebaser/src/dm/ve.dm.DocumentStore.js',
|
|
'rebaser/src/dm/ve.dm.PeerTransportServer.js',
|
|
'rebaser/src/dm/ve.dm.ProtocolServer.js',
|
|
'rebaser/src/dm/ve.dm.RebaseDocState.js',
|
|
'rebaser/src/dm/ve.dm.TransportServer.js',
|
|
'src/ve.track.js',
|
|
'src/ve.ext-peer.js',
|
|
'src/init/**/*.js',
|
|
// DM
|
|
'src/dm/ve.dm.InternalList.js',
|
|
'src/dm/ve.dm.SourceSurfaceFragment.js',
|
|
'src/dm/ve.dm.SurfaceSynchronizer.js',
|
|
'src/dm/ve.dm.TableSlice.js',
|
|
'src/dm/annotations/ve.dm.CommentAnnotation.js',
|
|
'src/dm/nodes/ve.dm.GeneratedContentNode.js',
|
|
'src/dm/nodes/ve.dm.HeadingNode.js',
|
|
'src/dm/nodes/ve.dm.ImageNode.js',
|
|
'src/dm/nodes/ve.dm.InternalItemNode.js',
|
|
// CE
|
|
'src/ce/annotations/ve.ce.DeleteAnnotation.js',
|
|
'src/ce/annotations/ve.ce.InsertAnnotation.js',
|
|
'src/ce/nodes/ve.ce.CheckListItemNode.js',
|
|
'src/ce/nodes/ve.ce.GeneratedContentNode.js',
|
|
'src/ce/nodes/ve.ce.InternalItemNode.js',
|
|
'src/ce/keydownhandlers/ve.ce.TableDeleteKeyDownHandler.js',
|
|
// UI
|
|
'src/ui/*.js',
|
|
'src/ui/actions/*.js',
|
|
'src/ui/commands/*.js',
|
|
'src/ui/contextitems/*.js',
|
|
'src/ui/contexts/*.js',
|
|
'src/ui/datatransferhandlers/*.js',
|
|
'src/ui/dialogs/*.js',
|
|
'src/ui/inspectors/ve.ui.CommentAnnotationInspector.js',
|
|
'src/ui/layouts/*.js',
|
|
'src/ui/pages/*.js',
|
|
'src/ui/tools/*.js',
|
|
'src/ui/widgets/*.js',
|
|
'src/ui/windowmanagers/*.js'
|
|
],
|
|
overrides: {
|
|
// Core
|
|
// TODO: Fix a few cases for 80% coverage
|
|
'src/*.js': coverAll( 50 ),
|
|
// DM
|
|
'src/dm/*.js': coverAll( 50 ),
|
|
'src/dm/annotations/*.js': coverAll( 100 ),
|
|
'src/dm/lineardata/*.js': coverAll( 95 ),
|
|
// TODO: Fix AlienMetaItem for 100% coverage
|
|
'src/dm/metaitems/*.js': coverAll( 50 ),
|
|
// TODO: Fix a few cases for 80% coverage
|
|
'src/dm/nodes/*.js': coverAll( 50 ),
|
|
// TODO: Fix a few cases for 95% coverage
|
|
'src/dm/selections/*.js': coverAll( 50 ),
|
|
// CE
|
|
'src/ce/*.js': coverAll( 50 ),
|
|
// TODO: Fix a few cases for 80% coverage
|
|
'src/ce/annotations/*.js': coverAll( 50 ),
|
|
'src/ce/keydownhandlers/*.js': coverAll( 80 ),
|
|
'src/ce/nodes/*.js': coverAll( 50 ),
|
|
// TODO: Fix a few cases for 80% coverage
|
|
'src/ce/selections/*.js': coverAll( 50 ),
|
|
// UI
|
|
'src/ui/elements/*.js': coverAll( 50 ),
|
|
'src/ui/inspectors/*.js': coverAll( 50 )
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Separate job for Firefox as we don't want a second coverage report.
|
|
firefox: {
|
|
browsers: [ 'FirefoxHeadless' ]
|
|
},
|
|
bg: {
|
|
browsers: [ 'Chrome', 'Firefox' ],
|
|
singleRun: false,
|
|
background: true
|
|
}
|
|
},
|
|
runwatch: {
|
|
files: [
|
|
'.{stylelintrc,eslintrc}.json',
|
|
'**/*.js',
|
|
'!coverage/**',
|
|
'!dist/**',
|
|
'!docs/**',
|
|
'!node_modules/**',
|
|
'<%= stylelint.all %>'
|
|
],
|
|
tasks: [ 'test', 'karma:bg:run' ]
|
|
}
|
|
} );
|
|
|
|
grunt.registerTask( 'git-status', function () {
|
|
const done = this.async();
|
|
// Are there unstaged changes?
|
|
require( 'child_process' ).exec( 'git ls-files --modified', function ( err, stdout, stderr ) {
|
|
const ret = err || stderr || stdout;
|
|
if ( ret ) {
|
|
grunt.log.error( 'Unstaged changes in these files:' );
|
|
grunt.log.error( ret );
|
|
// Show a condensed diff
|
|
require( 'child_process' ).exec( 'git diff -U1 | tail -n +3', function ( err2, stdout2, stderr2 ) {
|
|
grunt.log.write( err2 || stderr2 || stdout2 );
|
|
done( false );
|
|
} );
|
|
} else {
|
|
grunt.log.ok( 'No unstaged changes.' );
|
|
done();
|
|
}
|
|
} );
|
|
} );
|
|
|
|
grunt.registerTask( 'build', [ 'clean:dist', 'concat', 'less', 'clean:less', 'cssjanus', 'cssUrlEmbed', 'copy', 'buildloader' ] );
|
|
grunt.registerTask( 'lint', [ 'tyops', 'eslint', 'stylelint', 'banana' ] );
|
|
grunt.registerTask( 'unit', [ 'karma:chrome', 'karma:firefox' ] );
|
|
grunt.registerTask( '_test', [ 'lint', 'git-build', 'build', 'unit' ] );
|
|
grunt.registerTask( 'ci', [ '_test', 'git-status' ] );
|
|
grunt.registerTask( 'watch', [ 'karma:bg:start', 'runwatch' ] );
|
|
|
|
if ( process.env.JENKINS_HOME ) {
|
|
grunt.registerTask( 'test', 'ci' );
|
|
} else {
|
|
grunt.registerTask( 'test', '_test' );
|
|
}
|
|
|
|
grunt.registerTask( 'default', 'test' );
|
|
};
|