diff --git a/packages/ckeditor5-package-generator/lib/templates/sample/index.html b/packages/ckeditor5-package-generator/lib/templates/sample/index.html index afca4361..ce6d2f63 100644 --- a/packages/ckeditor5-package-generator/lib/templates/sample/index.html +++ b/packages/ckeditor5-package-generator/lib/templates/sample/index.html @@ -94,6 +94,6 @@

Reporting issues

- + diff --git a/packages/ckeditor5-package-tools/lib/utils/get-karma-config.js b/packages/ckeditor5-package-tools/lib/utils/get-karma-config.js index f6d1ddb9..35f2483e 100644 --- a/packages/ckeditor5-package-tools/lib/utils/get-karma-config.js +++ b/packages/ckeditor5-package-tools/lib/utils/get-karma-config.js @@ -9,6 +9,7 @@ const path = require( 'path' ); const { getPostCssConfig } = require( '@ckeditor/ckeditor5-dev-utils' ).styles; +const getThemePath = require( './get-theme-path' ); /** * @param {Object} options @@ -20,7 +21,7 @@ module.exports = options => { const karmaConfig = { basePath: options.cwd, - frameworks: [ 'mocha', 'sinon-chai' ], + frameworks: [ 'mocha', 'sinon-chai', 'webpack' ], files: [ options.entryFile ], @@ -133,7 +134,7 @@ function getWebpackConfiguration( options ) { rules: [ { test: /\.svg$/, - use: [ 'raw-loader' ] + use: 'raw-loader' }, { test: /\.css$/, @@ -150,20 +151,20 @@ function getWebpackConfiguration( options ) { 'css-loader', { loader: 'postcss-loader', - options: getPostCssConfig( { - themeImporter: { - themePath: require.resolve( - path.join( options.cwd, 'node_modules', '@ckeditor', 'ckeditor5-theme-lark' ) - ) - }, - minify: true - } ) + options: { + postcssOptions: getPostCssConfig( { + themeImporter: { + themePath: getThemePath( options.cwd ) + }, + minify: true + } ) + } } ] }, { test: /\.(txt|html|rtf)$/, - use: [ 'raw-loader' ] + use: 'raw-loader' } ] }, @@ -176,7 +177,8 @@ function getWebpackConfiguration( options ) { }; if ( options.sourceMap ) { - config.devtool = 'inline-source-map'; + // Available list: https://webpack.js.org/configuration/devtool/. + config.devtool = 'eval-cheap-module-source-map'; } if ( options.coverage ) { @@ -185,7 +187,7 @@ function getWebpackConfiguration( options ) { test: /\.js$/, loader: 'istanbul-instrumenter-loader', include: path.join( options.cwd, 'src' ), - query: { + options: { esModules: true } } diff --git a/packages/ckeditor5-package-tools/lib/utils/get-theme-path.js b/packages/ckeditor5-package-tools/lib/utils/get-theme-path.js new file mode 100644 index 00000000..cea23fd0 --- /dev/null +++ b/packages/ckeditor5-package-tools/lib/utils/get-theme-path.js @@ -0,0 +1,26 @@ +/** + * @license Copyright (c) 2020-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/* eslint-env node */ + +const path = require( 'path' ); + +/** + * Returns an absolute path to the main file of the `@ckeditor/ckeditor5-theme-lark` package. + * + * The function does the same as what does `require.resolve()`. However, there is no option for mocking it in tests, + * hence the value is obtained manually. + * + * @param {String} cwd + * @return {String} + */ +module.exports = function getThemePath( cwd ) { + const packagePath = path.join( cwd, 'node_modules', '@ckeditor', 'ckeditor5-theme-lark' ); + const packageJson = require( path.join( packagePath, 'package.json' ) ); + + return path.join( packagePath, packageJson.main ); +}; diff --git a/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-dll.js b/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-dll.js index 17939372..96af7911 100644 --- a/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-dll.js +++ b/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-dll.js @@ -14,6 +14,7 @@ const webpack = require( 'webpack' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' ); const { getPostCssConfig } = require( '@ckeditor/ckeditor5-dev-utils' ).styles; +const getThemePath = require( './get-theme-path' ); module.exports = options => { const packageJson = require( path.join( options.cwd, 'package.json' ) ); @@ -35,6 +36,10 @@ module.exports = options => { manifest: require( ckeditor5manifestPath ), scope: 'ckeditor5/src', name: 'CKEditor5.dll' + } ), + new webpack.ProvidePlugin( { + process: 'process/browser', + Buffer: [ 'buffer', 'Buffer' ] } ) ]; @@ -102,12 +107,14 @@ module.exports = options => { 'css-loader', { loader: 'postcss-loader', - options: getPostCssConfig( { - themeImporter: { - themePath: getThemePath( options.cwd ) - }, - minify: true - } ) + options: { + postcssOptions: getPostCssConfig( { + themeImporter: { + themePath: getThemePath( options.cwd ) + }, + minify: true + } ) + } } ] } @@ -115,19 +122,3 @@ module.exports = options => { } }; }; - -/** - * Returns an absolute path to the main file of the `@ckeditor/ckeditor5-theme-lark` package. - * - * The function does the same as what does `require.resolve()`. However, there is no option for mocking it in tests, - * hence the value is obtained manually. - * - * @param {String} cwd - * @return {String} - */ -function getThemePath( cwd ) { - const packagePath = path.join( cwd, 'node_modules', '@ckeditor', 'ckeditor5-theme-lark' ); - const packageJson = require( path.join( packagePath, 'package.json' ) ); - - return path.join( packagePath, packageJson.main ); -} diff --git a/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-server.js b/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-server.js index 2921b6c4..7901ab3d 100644 --- a/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-server.js +++ b/packages/ckeditor5-package-tools/lib/utils/get-webpack-config-server.js @@ -11,11 +11,16 @@ const path = require( 'path' ); const webpack = require( 'webpack' ); const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' ); const { getPostCssConfig } = require( '@ckeditor/ckeditor5-dev-utils' ).styles; +const getThemePath = require( './get-theme-path' ); module.exports = options => { const webpackPlugins = [ new webpack.DefinePlugin( { EDITOR_LANGUAGE: JSON.stringify( options.language ) + } ), + new webpack.ProvidePlugin( { + process: 'process/browser', + Buffer: [ 'buffer', 'Buffer' ] } ) ]; @@ -38,7 +43,7 @@ module.exports = options => { entry: path.join( options.cwd, 'sample', 'ckeditor.js' ), output: { - filename: 'script.dist.js', + filename: 'ckeditor.dist.js', path: path.join( options.cwd, 'sample' ) }, @@ -59,7 +64,7 @@ module.exports = options => { rules: [ { test: /\.svg$/, - use: [ 'raw-loader' ] + use: 'raw-loader' }, { test: /\.css$/, @@ -76,14 +81,14 @@ module.exports = options => { 'css-loader', { loader: 'postcss-loader', - options: getPostCssConfig( { - themeImporter: { - themePath: require.resolve( - path.join( options.cwd, 'node_modules', '@ckeditor', 'ckeditor5-theme-lark' ) - ) - }, - minify: true - } ) + options: { + postcssOptions: getPostCssConfig( { + themeImporter: { + themePath: getThemePath( options.cwd ) + }, + minify: true + } ) + } } ] } diff --git a/packages/ckeditor5-package-tools/package.json b/packages/ckeditor5-package-tools/package.json index b70a3c8b..23a71335 100644 --- a/packages/ckeditor5-package-tools/package.json +++ b/packages/ckeditor5-package-tools/package.json @@ -14,6 +14,7 @@ "@ckeditor/ckeditor5-dev-utils": "^26.0.0", "@ckeditor/ckeditor5-dev-env": "^26.0.0", "@ckeditor/ckeditor5-dev-webpack-plugin": "^26.0.0", + "buffer": "^6.0.3", "chai": "^4.3.4", "css-loader": "^5.2.7", "istanbul-instrumenter-loader": "^3.0.1", @@ -26,19 +27,21 @@ "karma-sinon": "^1.0.5", "karma-sinon-chai": "^2.0.2", "karma-sourcemap-loader": "^0.3.8", - "karma-webpack": "^4.0.2", + "karma-webpack": "^5.0.0", "minimist": "^1.2.5", "mocha": "^7.2.0", - "postcss-loader": "^3.0.0", + "process": "^0.11.10", + "postcss": "^7.0.39", + "postcss-loader": "^4.3.0", "raw-loader": "^4.0.2", "sinon": "^9.2.4", "sinon-chai": "^3.7.0", - "style-loader": "^1.3.0", + "style-loader": "^2.0.0", "stylelint": "^13.13.1", "stylelint-config-ckeditor5": "^2.0.1", "terser-webpack-plugin": "^3.0.2", - "webpack": "^4.46.0", - "webpack-dev-server": "^4.1.0" + "webpack": "^5.58.1", + "webpack-dev-server": "^4.3.1" }, "devDependencies": { "chai": "^4.3.4", diff --git a/packages/ckeditor5-package-tools/tests/utils/generate-entry-file.js b/packages/ckeditor5-package-tools/tests/utils/generate-entry-file.js index 41dfdc4c..27524556 100644 --- a/packages/ckeditor5-package-tools/tests/utils/generate-entry-file.js +++ b/packages/ckeditor5-package-tools/tests/utils/generate-entry-file.js @@ -54,10 +54,9 @@ describe( 'lib/utils/generate-entry-file', () => { } ); afterEach( () => { - mockery.deregisterAll(); - mockery.disable(); sinon.restore(); clock.restore(); + mockery.disable(); } ); it( 'should be a function', () => { diff --git a/packages/ckeditor5-package-tools/tests/utils/get-karma-config.js b/packages/ckeditor5-package-tools/tests/utils/get-karma-config.js index 047bb082..2847b51f 100644 --- a/packages/ckeditor5-package-tools/tests/utils/get-karma-config.js +++ b/packages/ckeditor5-package-tools/tests/utils/get-karma-config.js @@ -5,17 +5,286 @@ 'use strict'; +const sinon = require( 'sinon' ); +const mockery = require( 'mockery' ); const expect = require( 'chai' ).expect; describe( 'lib/utils/get-karma-config', () => { - let getKarmaConfig; + let getKarmaConfig, stubs; + + const cwd = '/process/cwd'; beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + packageJson: { + name: '@ckeditor/ckeditor5-foo' + }, + path: { + join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ), + resolve: sinon.stub().callsFake( file => `/process/cwd/${ file }` ) + }, + devUtils: { + styles: { + getPostCssConfig: sinon.stub() + } + }, + getThemePath: sinon.stub() + }; + + mockery.registerMock( 'path', stubs.path ); + mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); + mockery.registerMock( './get-theme-path', stubs.getThemePath ); + getKarmaConfig = require( '../../lib/utils/get-karma-config' ); } ); + afterEach( () => { + sinon.restore(); + mockery.disable(); + } ); + it( 'should be a function', () => { expect( getKarmaConfig ).to.be.a( 'function' ); } ); - // TODO: Add tests. + + it( 'defines "webpack" in the "#frameworks" property', () => { + const config = getKarmaConfig( { cwd } ); + + expect( config.frameworks ).to.be.an( 'array' ); + expect( config.frameworks ).to.contain( 'webpack' ); + } ); + + it( 'uses no sandbox version of Chrome when executing on CI', () => { + const envCI = process.env.CI; + process.env.CI = true; + + const config = getKarmaConfig( { cwd } ); + + expect( config.browsers ).to.deep.equal( [ 'CHROME_TRAVIS_CI' ] ); + + process.env.CI = envCI; + } ); + + it( 'enables watching source files when passed the watch option', () => { + const config = getKarmaConfig( { cwd, verbose: true } ); + + expect( config.webpackMiddleware ).to.deep.equal( { + noInfo: false + } ); + } ); + + it( 'enables printing webpack logs when passed the verbose option', () => { + const config = getKarmaConfig( { cwd, watch: true } ); + + expect( config.autoWatch ).to.equal( true ); + expect( config.singleRun ).to.equal( false ); + } ); + + describe( 'webpack configuration', () => { + describe( 'loaders', () => { + let webpackConfig; + + beforeEach( () => { + stubs.getThemePath.returns( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' ); + stubs.devUtils.styles.getPostCssConfig.returns( { foo: true } ); + + webpackConfig = getKarmaConfig( { cwd } ).webpack; + } ); + + describe( '*.svg', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'svg' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.equal( 'raw-loader' ); + } ); + + it( 'loads paths that end with the ".svg" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.svg' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.svg' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.css', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'css' ) ); + + expect( loader ).is.an( 'object' ); + expect( loader.use ).is.an( 'array' ); + expect( loader.use ).to.lengthOf( 3 ); + } ); + + // Webpack processes loaders from the bottom, to the top. Hence, "postcss-loader" will be called as the first one. + it( 'uses "postcss-loader" for processing CKEditor 5 assets', () => { + expect( stubs.getThemePath.calledOnce ).to.equal( true ); + expect( stubs.getThemePath.firstCall.args[ 0 ] ).to.equal( '/process/cwd' ); + + expect( stubs.devUtils.styles.getPostCssConfig.calledOnce ).to.equal( true ); + expect( stubs.devUtils.styles.getPostCssConfig.firstCall.firstArg ).to.deep.equal( { + minify: true, + themeImporter: { + themePath: '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' + } + } ); + + const postcssLoader = loader.use.slice( -1 ).pop(); + + expect( postcssLoader ).to.be.an( 'object' ); + + expect( postcssLoader ).to.haveOwnProperty( 'loader' ); + expect( postcssLoader.loader ).to.equal( 'postcss-loader' ); + + expect( postcssLoader ).to.haveOwnProperty( 'options' ); + expect( postcssLoader.options ).to.deep.equal( { + postcssOptions: { foo: true } + } ); + } ); + + it( 'uses "css-loader" to resolve imports from JS files', () => { + const cssLoader = loader.use.slice( 1, -1 ).pop(); + + expect( cssLoader ).to.equal( 'css-loader' ); + } ); + + it( 'uses "style-loader" for injecting processed styles on a page', () => { + const styleLoader = loader.use.slice( 0 ).shift(); + + expect( styleLoader ).to.haveOwnProperty( 'loader' ); + expect( styleLoader.loader ).to.equal( 'style-loader' ); + + expect( styleLoader ).to.haveOwnProperty( 'options' ); + expect( styleLoader.options ).to.deep.equal( { + injectType: 'singletonStyleTag', + attributes: { + 'data-cke': true + } + } ); + } ); + + it( 'loads paths that end with the ".css" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.html' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.html' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.txt', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'txt' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.equal( 'raw-loader' ); + } ); + + it( 'loads paths that end with the ".txt" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/assets/ckeditor.txt' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\assets\\ckeditor.txt' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.html', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'html' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.equal( 'raw-loader' ); + } ); + + it( 'loads paths that end with the ".html" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/assets/ckeditor.html' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\assets\\ckeditor.html' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.rtf', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'rtf' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.equal( 'raw-loader' ); + } ); + + it( 'loads paths that end with the ".rtf" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/assets/ckeditor.rtf' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\assets\\ckeditor.rtf' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.js (code coverage)', () => { + let loader; + + beforeEach( () => { + webpackConfig = getKarmaConfig( { cwd, coverage: true } ).webpack; + + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'js' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "istanbul-instrumenter-loader" for providing files', () => { + expect( loader.loader ).to.equal( 'istanbul-instrumenter-loader' ); + expect( loader.include ).to.equal( '/process/cwd/src' ); + expect( loader.options ).to.deep.equal( { + esModules: true + } ); + } ); + + it( 'loads paths that end with the ".js" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/src/ckeditor.js' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\src\\ckeditor.js' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + } ); + + it( 'allows enabling source maps', () => { + const config = getKarmaConfig( { cwd, sourceMap: true } ); + + expect( config.webpack.devtool ).to.equal( 'eval-cheap-module-source-map' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-package-tools/tests/utils/get-theme-path.js b/packages/ckeditor5-package-tools/tests/utils/get-theme-path.js new file mode 100644 index 00000000..64947390 --- /dev/null +++ b/packages/ckeditor5-package-tools/tests/utils/get-theme-path.js @@ -0,0 +1,52 @@ +/** + * @license Copyright (c) 2020-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +const mockery = require( 'mockery' ); +const sinon = require( 'sinon' ); +const expect = require( 'chai' ).expect; + +describe( 'lib/utils/get-theme-path', () => { + let getThemePath, stubs; + + const cwd = '/process/cwd'; + + beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + packageJson: { + name: '@ckeditor/ckeditor5-theme-lark', + main: './theme/theme.css' + }, + path: { + join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ).replace( '/./', '/' ) ) + } + }; + + mockery.registerMock( 'path', stubs.path ); + mockery.registerMock( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme-lark/package.json', stubs.packageJson ); + + getThemePath = require( '../../lib/utils/get-theme-path' ); + } ); + + afterEach( () => { + sinon.restore(); + mockery.disable(); + } ); + + it( 'should be a function', () => { + expect( getThemePath ).to.be.a( 'function' ); + } ); + + it( 'returns an absolute path to an entry file of the "@ckeditor/ckeditor5-theme-lark" package', () => { + expect( getThemePath( cwd ) ).to.equal( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme-lark/theme/theme.css' ); + } ); +} ); diff --git a/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-dll.js b/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-dll.js index 0c58c0f5..9d9f4542 100644 --- a/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-dll.js +++ b/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-dll.js @@ -29,31 +29,48 @@ describe( 'lib/utils/get-webpack-config-dll', () => { name: 'CKEditor5.dll', content: {} }, + fs: { + existsSync: sinon.stub() + }, path: { - join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ), - resolve: sinon.stub().callsFake( file => `/process/cwd/${ file }` ) + join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ) + }, + devUtils: { + styles: { + getPostCssConfig: sinon.stub() + } }, + getThemePath: sinon.stub(), webpack: sinon.stub(), - dllReferencePlugin: sinon.stub() + dllReferencePlugin: sinon.stub(), + providePlugin: sinon.stub(), + devWebpackPlugin: sinon.stub() }; stubs.webpack.DllReferencePlugin = function( ...args ) { return stubs.dllReferencePlugin( ...args ); }; + stubs.webpack.ProvidePlugin = function( ...args ) { + return stubs.providePlugin( ...args ); + }; + + stubs.fs.existsSync.returns( false ); + mockery.registerMock( 'fs', stubs.fs ); mockery.registerMock( 'path', stubs.path ); mockery.registerMock( 'webpack', stubs.webpack ); + mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); + mockery.registerMock( '@ckeditor/ckeditor5-dev-webpack-plugin', stubs.devWebpackPlugin ); + mockery.registerMock( './get-theme-path', stubs.getThemePath ); + mockery.registerMock( '/process/cwd/node_modules/ckeditor5/build/ckeditor5-dll.manifest.json', stubs.ckeditor5manifest ); - mockery.registerMock( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme-lark/package.json', { - main: './theme/theme.css' - } ); mockery.registerMock( '/process/cwd/package.json', stubs.packageJson ); getWebpackConfigDll = require( '../../lib/utils/get-webpack-config-dll' ); } ); afterEach( () => { - mockery.deregisterAll(); + sinon.restore(); mockery.disable(); } ); @@ -61,6 +78,46 @@ describe( 'lib/utils/get-webpack-config-dll', () => { expect( getWebpackConfigDll ).to.be.a( 'function' ); } ); + it( 'loads "process" polyfill for webpack 5', () => { + getWebpackConfigDll( { cwd } ); + + expect( stubs.providePlugin.calledOnce ).to.equal( true ); + expect( stubs.providePlugin.firstCall.firstArg ).to.be.an( 'object' ); + expect( stubs.providePlugin.firstCall.firstArg ).to.have.property( 'process', 'process/browser' ); + } ); + + it( 'loads "Buffer" polyfill for webpack 5', () => { + getWebpackConfigDll( { cwd } ); + + expect( stubs.providePlugin.calledOnce ).to.equal( true ); + expect( stubs.providePlugin.firstCall.firstArg ).to.be.an( 'object' ); + expect( stubs.providePlugin.firstCall.firstArg ).to.have.property( 'Buffer' ); + expect( stubs.providePlugin.firstCall.firstArg.Buffer ).to.deep.equal( [ 'buffer', 'Buffer' ] ); + } ); + + it( 'allows watching source files', () => { + const config = getWebpackConfigDll( { cwd, watch: true } ); + + expect( config.watch ).to.equal( true ); + } ); + + it( 'produces translation files based on "*.po" files', () => { + stubs.fs.existsSync.returns( true ); + + getWebpackConfigDll( { cwd } ); + + expect( stubs.fs.existsSync.calledOnce ).to.equal( true ); + expect( stubs.fs.existsSync.firstCall.firstArg ).to.equal( '/process/cwd/lang/translations/en.po' ); + + expect( stubs.devWebpackPlugin.calledOnce ).to.equal( true ); + expect( stubs.devWebpackPlugin.firstCall.firstArg ).to.deep.equal( { + additionalLanguages: 'all', + language: 'en', + skipPluralFormFunction: true, + sourceFilesPattern: /^src[/\\].+\.js$/ + } ); + } ); + describe( 'verifying exposed DLL names', () => { it( 'returns "foo" for a window key and a file name for the "@ckeditor/ckeditor5-foo" package', () => { const webpackConfig = getWebpackConfigDll( { cwd } ); @@ -78,5 +135,104 @@ describe( 'lib/utils/get-webpack-config-dll', () => { expect( webpackConfig.output.library ).to.deep.equal( [ 'CKEditor5', 'fooBar' ] ); } ); } ); - // TODO: Add more tests. + + describe( 'loaders', () => { + let webpackConfig; + + beforeEach( () => { + stubs.getThemePath.returns( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' ); + stubs.devUtils.styles.getPostCssConfig.returns( { foo: true } ); + + webpackConfig = getWebpackConfigDll( { cwd } ); + } ); + + describe( '*.svg', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'svg' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.deep.equal( [ 'raw-loader' ] ); + } ); + + it( 'loads paths that end with the ".svg" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.svg' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.svg' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.css', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'css' ) ); + + expect( loader ).is.an( 'object' ); + expect( loader.use ).is.an( 'array' ); + expect( loader.use ).to.lengthOf( 3 ); + } ); + + // Webpack processes loaders from the bottom, to the top. Hence, "postcss-loader" will be called as the first one. + it( 'uses "postcss-loader" for processing CKEditor 5 assets', () => { + expect( stubs.getThemePath.calledOnce ).to.equal( true ); + expect( stubs.getThemePath.firstCall.args[ 0 ] ).to.equal( '/process/cwd' ); + + expect( stubs.devUtils.styles.getPostCssConfig.calledOnce ).to.equal( true ); + expect( stubs.devUtils.styles.getPostCssConfig.firstCall.firstArg ).to.deep.equal( { + minify: true, + themeImporter: { + themePath: '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' + } + } ); + + const postcssLoader = loader.use.slice( -1 ).pop(); + + expect( postcssLoader ).to.be.an( 'object' ); + + expect( postcssLoader ).to.haveOwnProperty( 'loader' ); + expect( postcssLoader.loader ).to.equal( 'postcss-loader' ); + + expect( postcssLoader ).to.haveOwnProperty( 'options' ); + expect( postcssLoader.options ).to.deep.equal( { + postcssOptions: { foo: true } + } ); + } ); + + it( 'uses "css-loader" to resolve imports from JS files', () => { + const cssLoader = loader.use.slice( 1, -1 ).pop(); + + expect( cssLoader ).to.equal( 'css-loader' ); + } ); + + it( 'uses "style-loader" for injecting processed styles on a page', () => { + const styleLoader = loader.use.slice( 0 ).shift(); + + expect( styleLoader ).to.haveOwnProperty( 'loader' ); + expect( styleLoader.loader ).to.equal( 'style-loader' ); + + expect( styleLoader ).to.haveOwnProperty( 'options' ); + expect( styleLoader.options ).to.deep.equal( { + injectType: 'singletonStyleTag', + attributes: { + 'data-cke': true + } + } ); + } ); + + it( 'loads paths that end with the ".css" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.html' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.html' ).to.not.match( loader.test ); + } ); + } ); + } ); } ); diff --git a/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-server.js b/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-server.js index 2d944340..89cf3591 100644 --- a/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-server.js +++ b/packages/ckeditor5-package-tools/tests/utils/get-webpack-config-server.js @@ -5,17 +5,229 @@ 'use strict'; +const mockery = require( 'mockery' ); +const sinon = require( 'sinon' ); const expect = require( 'chai' ).expect; describe( 'lib/utils/get-webpack-config-server', () => { - let getWebpackConfigServer; + let getWebpackConfigServer, stubs; + + const cwd = '/process/cwd'; beforeEach( () => { + mockery.enable( { + useCleanCache: true, + warnOnReplace: false, + warnOnUnregistered: false + } ); + + stubs = { + packageJson: { + name: '@ckeditor/ckeditor5-foo' + }, + path: { + join: sinon.stub().callsFake( ( ...chunks ) => chunks.join( '/' ) ), + resolve: sinon.stub().callsFake( file => `/process/cwd/${ file }` ) + }, + devUtils: { + styles: { + getPostCssConfig: sinon.stub() + } + }, + getThemePath: sinon.stub(), + webpack: sinon.stub(), + providePlugin: sinon.stub(), + definePlugin: sinon.stub(), + devWebpackPlugin: sinon.stub() + }; + + stubs.webpack.ProvidePlugin = function( ...args ) { + return stubs.providePlugin( ...args ); + }; + + stubs.webpack.DefinePlugin = function( ...args ) { + return stubs.definePlugin( ...args ); + }; + + mockery.registerMock( 'path', stubs.path ); + mockery.registerMock( 'webpack', stubs.webpack ); + mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', stubs.devUtils ); + mockery.registerMock( '@ckeditor/ckeditor5-dev-webpack-plugin', stubs.devWebpackPlugin ); + mockery.registerMock( './get-theme-path', stubs.getThemePath ); + getWebpackConfigServer = require( '../../lib/utils/get-webpack-config-server' ); } ); + afterEach( () => { + sinon.restore(); + mockery.disable(); + } ); + it( 'should be a function', () => { expect( getWebpackConfigServer ).to.be.a( 'function' ); } ); - // TODO: Add tests. + + it( 'processes the "ckeditor.js" file', () => { + const config = getWebpackConfigServer( { cwd } ); + + expect( config.entry ).to.equal( '/process/cwd/sample/ckeditor.js' ); + expect( config.output ).to.deep.equal( { + filename: 'ckeditor.dist.js', + path: '/process/cwd/sample' + } ); + } ); + + it( 'loads "process" polyfill for webpack 5', () => { + getWebpackConfigServer( { cwd } ); + + expect( stubs.providePlugin.calledOnce ).to.equal( true ); + expect( stubs.providePlugin.firstCall.firstArg ).to.be.an( 'object' ); + expect( stubs.providePlugin.firstCall.firstArg ).to.have.property( 'process', 'process/browser' ); + } ); + + it( 'loads "Buffer" polyfill for webpack 5', () => { + getWebpackConfigServer( { cwd } ); + + expect( stubs.providePlugin.calledOnce ).to.equal( true ); + expect( stubs.providePlugin.firstCall.firstArg ).to.be.an( 'object' ); + expect( stubs.providePlugin.firstCall.firstArg ).to.have.property( 'Buffer' ); + expect( stubs.providePlugin.firstCall.firstArg.Buffer ).to.deep.equal( [ 'buffer', 'Buffer' ] ); + } ); + + it( 'defines the configuration for an HTTP server', () => { + const config = getWebpackConfigServer( { cwd } ); + + expect( config.devServer ).to.deep.equal( { + static: { + directory: '/process/cwd/sample' + }, + compress: true + } ); + } ); + + describe( 'sample language', () => { + it( 'passes the specified language directly to source file', () => { + getWebpackConfigServer( { cwd, language: 'en' } ); + + expect( stubs.definePlugin.calledOnce ).to.equal( true ); + expect( stubs.definePlugin.firstCall.firstArg ).to.deep.equal( { + EDITOR_LANGUAGE: '"en"' + } ); + } ); + + it( 'enables producing translations for non-English editors', () => { + getWebpackConfigServer( { cwd, language: 'pl' } ); + + expect( stubs.definePlugin.calledOnce ).to.equal( true ); + expect( stubs.definePlugin.firstCall.firstArg ).to.deep.equal( { + EDITOR_LANGUAGE: '"pl"' + } ); + + expect( stubs.devWebpackPlugin.calledOnce ).to.equal( true ); + expect( stubs.devWebpackPlugin.firstCall.firstArg ).to.deep.equal( { + language: 'pl', + sourceFilesPattern: /src[/\\].+\.js$/ + } ); + } ); + } ); + + describe( 'loaders', () => { + let webpackConfig; + + beforeEach( () => { + stubs.getThemePath.returns( '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' ); + stubs.devUtils.styles.getPostCssConfig.returns( { foo: true } ); + + webpackConfig = getWebpackConfigServer( { cwd } ); + } ); + + describe( '*.svg', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'svg' ) ); + + expect( loader ).is.an( 'object' ); + } ); + + it( 'uses "raw-loader" for providing files', () => { + expect( loader.use ).to.equal( 'raw-loader' ); + } ); + + it( 'loads paths that end with the ".svg" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.svg' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.svg' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.not.match( loader.test ); + } ); + } ); + + describe( '*.css', () => { + let loader; + + beforeEach( () => { + loader = webpackConfig.module.rules.find( loader => loader.test.toString().includes( 'css' ) ); + + expect( loader ).is.an( 'object' ); + expect( loader.use ).is.an( 'array' ); + expect( loader.use ).to.lengthOf( 3 ); + } ); + + // Webpack processes loaders from the bottom, to the top. Hence, "postcss-loader" will be called as the first one. + it( 'uses "postcss-loader" for processing CKEditor 5 assets', () => { + expect( stubs.getThemePath.calledOnce ).to.equal( true ); + expect( stubs.getThemePath.firstCall.args[ 0 ] ).to.equal( '/process/cwd' ); + + expect( stubs.devUtils.styles.getPostCssConfig.calledOnce ).to.equal( true ); + expect( stubs.devUtils.styles.getPostCssConfig.firstCall.firstArg ).to.deep.equal( { + minify: true, + themeImporter: { + themePath: '/process/cwd/node_modules/@ckeditor/ckeditor5-theme/theme/theme.css' + } + } ); + + const postcssLoader = loader.use.slice( -1 ).pop(); + + expect( postcssLoader ).to.be.an( 'object' ); + + expect( postcssLoader ).to.haveOwnProperty( 'loader' ); + expect( postcssLoader.loader ).to.equal( 'postcss-loader' ); + + expect( postcssLoader ).to.haveOwnProperty( 'options' ); + expect( postcssLoader.options ).to.deep.equal( { + postcssOptions: { foo: true } + } ); + } ); + + it( 'uses "css-loader" to resolve imports from JS files', () => { + const cssLoader = loader.use.slice( 1, -1 ).pop(); + + expect( cssLoader ).to.equal( 'css-loader' ); + } ); + + it( 'uses "style-loader" for injecting processed styles on a page', () => { + const styleLoader = loader.use.slice( 0 ).shift(); + + expect( styleLoader ).to.haveOwnProperty( 'loader' ); + expect( styleLoader.loader ).to.equal( 'style-loader' ); + + expect( styleLoader ).to.haveOwnProperty( 'options' ); + expect( styleLoader.options ).to.deep.equal( { + injectType: 'singletonStyleTag', + attributes: { + 'data-cke': true + } + } ); + } ); + + it( 'loads paths that end with the ".css" suffix', () => { + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.css' ).to.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.css' ).to.match( loader.test ); + + expect( '/Users/ckeditor/ckeditor5-foo/theme/icons/ckeditor.html' ).to.not.match( loader.test ); + expect( 'C:\\Users\\ckeditor\\ckeditor5-foo\\theme\\icons\\ckeditor.html' ).to.not.match( loader.test ); + } ); + } ); + } ); } ); diff --git a/scripts/ci/travis-check.js b/scripts/ci/travis-check.js index 57952724..f3cd0b6b 100644 --- a/scripts/ci/travis-check.js +++ b/scripts/ci/travis-check.js @@ -121,7 +121,7 @@ function startDevelopmentServer( cwd ) { // Hence, we can assume that the server is live at this stage. sampleServer.stdout.on( 'data', data => { const content = data.toString().slice( 0, -1 ); - const endMatch = /\+ \d+ hidden modules/.test( content ); + const endMatch = /webpack \d+\.\d+\.\d+ compiled successfully in \d+ ms/.test( content ); if ( endMatch ) { return resolve( {