├── .gitignore ├── LICENSE ├── Readme.md ├── index.html ├── package.json ├── resources ├── img │ └── favicon-src.png └── locales │ └── en-GB │ └── en-GB.json ├── src ├── Application.js ├── bootstrap.js ├── core │ └── field │ │ └── Combobox.js ├── overrides │ ├── Dialog.js │ └── index.js └── view │ ├── Main.js │ └── Main.scss ├── themes └── theme-material │ ├── all-debug.js │ └── all.scss ├── webpack ├── common.js ├── css.config.js ├── dev.js ├── esbuild.config.js ├── postcss.config.js ├── prod-server.js ├── prod.js └── settings.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /build 3 | /ext 4 | node_modules 5 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Don Griffin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Ext JS webpack App 2 | 3 | A simple demonstration of using [webpack](https://webpack.js.org/) to build an 4 | app bundle to go along with the pre-built Ext JS library. 5 | 6 | The goal here is to use fully modern build tools, avoiding Sencha Cmd, so that 7 | we get `import`, `export` and sourcemaps. 8 | 9 | ## Getting Started 10 | 11 | Make an `ext` folder that links to your Ext JS download content. In the 12 | checkout directory, run a command like this: 13 | 14 | $ ln -s ../../sdks/ext-6.5.3 ext 15 | 16 | On Windows: 17 | 18 | > mklink /D ext ..\..\sdks\ext-6.5.3 19 | 20 | As alternative, in case you need a custom ExtJS build (like upgrading FontAwesome to Pro), 21 | in the ExtJS release directory `ext-6.5.3/` run `sench package build` to build ExtJS files. Then after build, 22 | you can copy in `project/ext/build` the `ext-6.5.3/build/modern`, `ext-6.5.3/build/packages`, 23 | and `ext-6.5.3/build/ext-modern-all.js`, `-debug.js`. 24 | files into your project. 25 | 26 | Then run `yarn run build`: 27 | 28 | $ yarn run build 29 | 30 | > app@2.0.0 build ..../extjs-webpack 31 | > webpack 32 | 33 | Hash: 514ce3b213ceda0e6dbe 34 | Version: webpack 4.42.1 35 | Time: 99ms 36 | Built at: 04/05/2020 6:17:04 PM 37 | Asset Size Chunks Chunk Names 38 | bundle.js 4.98 KiB main [emitted] main 39 | Entrypoint main = bundle.js 40 | [./src/app.js] 300 bytes {main} [built] 41 | [./src/view/Main.js] 245 bytes {main} [built] 42 | 43 | That's all. Now load in browser. 44 | 45 | ## Going Forward 46 | 47 | First thing to note is that all app files are modules and you `import` them. They can 48 | export their constructors like I did in `Main.js`: 49 | 50 | export default 51 | Ext.define('App.view.Main, { 52 | ... 53 | }); 54 | 55 | The `Ext.define()` function needs to be used to extend Ext JS classes and it returns 56 | the class constructor. We export it so that other modules can get it in an ES6 way 57 | as I do in `app.js`: 58 | 59 | import Main from './view/Main.js' 60 | 61 | The class is still placed on the global scope as `App.view.Main`, but following the 62 | ES6 way will clarify relationships for the bundler. 63 | 64 | ### Strict Mode 65 | 66 | Modules use strict mode, so be aware that `callParent` cannot be called from a 67 | function declared in such places. This is easy to work around as I do in `Main`: 68 | 69 | const Panel = Ext.Panel; 70 | const superCls = Panel.prototype; 71 | 72 | export default 73 | Ext.define('App.view.Main', { 74 | extend: Panel, 75 | 76 | title : 'App', 77 | 78 | constructor(...args) { 79 | superCls.constructor.call(this, ...args); 80 | } 81 | }); 82 | 83 | Normally, in Ext JS, the `constructor` override would have used `callParent` to 84 | call the `Panel` super class. 85 | 86 | ## Conclusion 87 | 88 | Using Sencha Themer would be great partner here. Just build your theme and link 89 | it in via the index file. 90 | 91 | I'd love to hear what folks think of this approach! 92 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | App 8 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "4.0.0", 4 | "description": "Ext JS webpack App", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev": "webpack serve --config webpack/dev.js --node-env=development", 8 | "build": "webpack --config webpack/prod.js --progress --node-env=production", 9 | "prod": "webpack serve --config webpack/prod-server.js", 10 | "cy:open": "cypress open", 11 | "cy:run": "cypress run" 12 | }, 13 | "author": "Vadim Popa", 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">=16.13.0", 17 | "npm": ">=8.1.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:vadimpopa/extjs-webpack.git" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.16.0", 25 | "@babel/preset-env": "^7.16.4", 26 | "@cypress/webpack-dev-server": "^1.7.0", 27 | "@gfx/zopfli": "^1.0.15", 28 | "@vue/preload-webpack-plugin": "^2.0.0", 29 | "babel-loader": "^8.2.3", 30 | "clean-webpack-plugin": "^4.0.0", 31 | "copy-webpack-plugin": "^10.0.0", 32 | "core-js": "^3.19.3", 33 | "css-loader": "^6.5.1", 34 | "cssnano": "^5.0.12", 35 | "cypress": "9.2.0", 36 | "cypress-webpack-preprocessor-v5": "5.0.0-alpha.1", 37 | "dotenv": "^10.0.0", 38 | "esbuild-loader": "^2.16.0", 39 | "exports-loader": "^3.1.0", 40 | "git-rev-sync": "^3.0.1", 41 | "html-webpack-plugin": "^5.5.0", 42 | "husky": "7.0.4", 43 | "imports-loader": "^3.1.1", 44 | "lint-staged": "^12.1.2", 45 | "mini-css-extract-plugin": "^2.4.5", 46 | "moment": "^2.29.1", 47 | "postcss": "^8.4.4", 48 | "postcss-import": "^14.0.2", 49 | "postcss-loader": "^6.2.1", 50 | "postcss-preset-env": "^7.0.1", 51 | "prettier": "^2.5.1", 52 | "pretty-quick": "3.1.3", 53 | "sass": "^1.45.2", 54 | "sass-loader": "^12.4.0", 55 | "style-loader": "^3.3.1", 56 | "symlink-webpack-plugin": "^1.0.0", 57 | "terser-webpack-plugin": "^5.2.5", 58 | "val-loader": "^4.0.0", 59 | "webpack": "^5.65.0", 60 | "webpack-bundle-analyzer": "^4.5.0", 61 | "webpack-cli": "^4.9.1", 62 | "webpack-dev-server": "4.7.2", 63 | "webpack-merge": "^5.8.0", 64 | "webpack-notifier": "^1.14.1", 65 | "webpack-strip-block": "^0.3.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/img/favicon-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vadimpopa/extjs-webpack/7cfeaaf25ed5126447e0237599e5a39804150620/resources/img/favicon-src.png -------------------------------------------------------------------------------- /resources/locales/en-GB/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "My App", 3 | "wantDarkTheme": "Want Dark theme ?" 4 | } -------------------------------------------------------------------------------- /src/Application.js: -------------------------------------------------------------------------------- 1 | import './view/Main.js'; 2 | 3 | export default Ext.define('MyApp.Application', { 4 | extend: 'Ext.app.Application', 5 | 6 | namespaces: 'MyApp', 7 | name: 'MyApp', 8 | mainView: 'MyApp.view.Main', 9 | 10 | setStateItem(item, value) { 11 | debugger 12 | MyApp.Boot.getAppStateStorage().setItem(item, value); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const bootstrap = { 3 | lang: '', 4 | appName: 'MyApp', 5 | translations: {}, 6 | 7 | /** 8 | * 9 | * @return {Object} 10 | */ 11 | getAppStateStorage() { 12 | return Ext.util.LocalStorage.get(`${bootstrap.appName}State`); 13 | }, 14 | 15 | getApp() { 16 | let app = window[bootstrap.appName]; 17 | 18 | if (!app) { 19 | app = window[bootstrap.appName] = {}; 20 | } 21 | 22 | return app; 23 | }, 24 | 25 | getNavigatorLanguages() { 26 | let found = []; 27 | 28 | if (typeof navigator !== 'undefined') { 29 | if (navigator.languages) { 30 | // chrome only; not an array, so can't use .push.apply instead of iterating 31 | for (let i = 0; i < navigator.languages.length; i++) { 32 | found.push(navigator.languages[i]); 33 | } 34 | } 35 | if (navigator.userLanguage) { 36 | found.push(navigator.userLanguage); 37 | } 38 | if (navigator.language) { 39 | found.push(navigator.language); 40 | } 41 | } 42 | 43 | return found.length > 0 ? found : undefined; 44 | }, 45 | 46 | /** 47 | * @return {String} 48 | */ 49 | getLang() { 50 | let {lang} = bootstrap; 51 | 52 | if (!lang) { 53 | // First check localeStorage for saved language 54 | const localStorageInstance = bootstrap.getAppStateStorage(); 55 | const isAutoDetectLanguage = true; 56 | 57 | lang = localStorageInstance.getItem('lang'); 58 | localStorageInstance.release(); 59 | 60 | // If saved language is not found in locales auto detect from OS or default 61 | if (!lang) { 62 | if (isAutoDetectLanguage) { 63 | const languages = bootstrap.getNavigatorLanguages(); 64 | const found = languages.find((lang) => lang); 65 | 66 | if (found) { 67 | lang = found; 68 | } 69 | } 70 | } 71 | 72 | if (!lang) { 73 | lang = 'en'; 74 | } 75 | 76 | document.documentElement.setAttribute('lang', lang); 77 | 78 | bootstrap.lang = lang; 79 | } 80 | 81 | return lang; 82 | }, 83 | 84 | initLang() { 85 | bootstrap.lang = ''; 86 | bootstrap.getLang(); 87 | }, 88 | 89 | isDarkMode() { 90 | const localStorageInstance = bootstrap.getAppStateStorage(); 91 | let darkMode = localStorageInstance.getItem('theme-dark-mode'); 92 | localStorageInstance.release(); 93 | 94 | return darkMode === 'true'; 95 | }, 96 | 97 | loadTranslations() { 98 | const me = this; 99 | const {lang} = me; 100 | 101 | return new Promise((resolve, reject) => { 102 | Ext.Ajax.request({ 103 | url: `resources/locales/${lang}/${lang}.json`, 104 | callback(options, success, response) { 105 | const data = Ext.decode(response.responseText, true); 106 | 107 | if (data) { 108 | me.translations = data; 109 | resolve(); 110 | } else { 111 | reject(); 112 | } 113 | }, 114 | }); 115 | }).catch(() => { 116 | const m = new Ext.MessageBox(); 117 | m.alert('Failure', `Couldn't load files`); 118 | }); 119 | }, 120 | 121 | i18nInit({default: i18next}) { 122 | const me = this; 123 | const {lang} = me; 124 | 125 | return i18next 126 | .init({ 127 | lng: lang, 128 | debug: true, 129 | fallbackLng: false, 130 | returnEmptyString: false, 131 | interpolation: { 132 | prefix: '{{', 133 | suffix: '}}', 134 | }, 135 | ns: ['app'], 136 | nsSeparator: false, 137 | keySeparator: false, 138 | defaultNS: 'app', 139 | }) 140 | .then(() => { 141 | i18next.addResourceBundle(lang, 'app', me.translations); 142 | 143 | const app = bootstrap.getApp(); 144 | 145 | app.i18next = i18next; 146 | app.t = i18next.t.bind(i18next); 147 | me.translations = null; 148 | }); 149 | } 150 | }; 151 | 152 | let themeName; 153 | let environment; 154 | 155 | const Ext = await import(/* webpackChunkName: "ext" */ 'Ext'); 156 | const app = bootstrap.getApp(); 157 | const {appName} = bootstrap; 158 | const localStorageInstance = bootstrap.getAppStateStorage(); 159 | 160 | Ext.ns(appName); 161 | 162 | app.Boot = bootstrap; 163 | app.isDarkMode = bootstrap.isDarkMode; 164 | 165 | themeName = localStorageInstance.getItem('theme') || 'theme-material'; 166 | environment = process.env.NODE_ENV; 167 | 168 | const {manifest} = Ext; 169 | 170 | manifest.env = environment; 171 | manifest.isDevelopment = environment === 'development'; 172 | 173 | await import( 174 | /* webpackChunkName: '[request]' */ 175 | `../themes/${themeName}/all${ 176 | environment === 'development' ? '-debug' : '' 177 | }.js` 178 | ).then(bootstrap.initLang) 179 | 180 | await bootstrap.loadTranslations(); 181 | 182 | await import('i18next').then(bootstrap.i18nInit.bind(bootstrap)); 183 | 184 | await import(/* webpackChunkName: "app" */ './overrides/index'); 185 | await import(/* webpackChunkName: "app" */ './Application'); 186 | 187 | Ext.application(`${appName}.Application`); 188 | })(); 189 | -------------------------------------------------------------------------------- /src/core/field/Combobox.js: -------------------------------------------------------------------------------- 1 | export default Ext.define('App.core.field.Combobox', { 2 | extend: 'Ext.field.ComboBox', 3 | 4 | clearable: true, 5 | forceSelection: true, 6 | 7 | config: { 8 | allDisplayTpl: false 9 | }, 10 | allDisplayTplRegex: /[^{\}]+(?=})/g, 11 | 12 | applyAllDisplayTpl(tpl) { 13 | if (tpl !== false) { 14 | this.setItemTpl(tpl); 15 | this.setDisplayTpl(tpl); 16 | this.setPrimaryFilter(new Ext.util.Filter({ 17 | property: tpl.match(this.allDisplayTplRegex), 18 | filterFn: this.allFilterFn 19 | })); 20 | } 21 | return tpl; 22 | }, 23 | 24 | allFilterFn(candidateRecord) { 25 | // This called in the scope of the Filter instance, we have this config 26 | let searchValue = this.getValue(); 27 | 28 | if (searchValue) { 29 | searchValue = searchValue.toLowerCase(); 30 | 31 | return this.getProperty().some(field => { 32 | const value = (candidateRecord.get(field) || '').toLowerCase(); 33 | return value.indexOf(searchValue) > -1; 34 | }); 35 | } 36 | return false; 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/overrides/Dialog.js: -------------------------------------------------------------------------------- 1 | const Dialog = Ext.Dialog; 2 | const superCls = Dialog.prototype; 3 | const initialize = superCls.initialize; 4 | 5 | Ext.define(null, { 6 | override: 'Ext.Dialog', 7 | 8 | initialize() { 9 | initialize.call(this); 10 | 11 | if (this.autoShow) { 12 | this.show(); 13 | } 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/overrides/index.js: -------------------------------------------------------------------------------- 1 | import './Dialog'; -------------------------------------------------------------------------------- /src/view/Main.js: -------------------------------------------------------------------------------- 1 | import './Main.scss'; 2 | import '../core/field/Combobox'; 3 | 4 | const Panel = Ext.Panel; 5 | const superclass = Panel.prototype; 6 | 7 | export default Ext.define('MyApp.view.Main', { 8 | extend: Panel, 9 | 10 | title: MyApp.t('appTitle'), 11 | 12 | html: `
Left Panel, 1/3rd of total size>
`, 13 | 14 | constructor(...args) { 15 | superclass.constructor.call(this, ...args); 16 | }, 17 | 18 | items: [ 19 | { 20 | xtype: 'checkbox', 21 | boxLabel: MyApp.t('wantDarkTheme'), 22 | listeners: { 23 | change(field, isDarkMode) { 24 | debugger 25 | MyApp.app.setStateItem('theme-dark-mode', isDarkMode); 26 | } 27 | } 28 | }, 29 | { 30 | xclass: 'App.core.field.Combobox', 31 | width: 400, 32 | margin: 5, 33 | label: 'Choose Language', 34 | queryMode: 'local', 35 | displayField: 'name', 36 | valueField: 'abbr', 37 | allDisplayTpl: '{abbr} {name}', 38 | store: [ 39 | {abbr: 'AL', name: 'Alabama'}, 40 | {abbr: 'AK', name: 'Alaska'}, 41 | {abbr: 'AZ', name: 'Arizona'} 42 | ] 43 | } 44 | ] 45 | }); 46 | -------------------------------------------------------------------------------- /src/view/Main.scss: -------------------------------------------------------------------------------- 1 | .my-class-from-sass { 2 | color: #0a0a0a; 3 | } -------------------------------------------------------------------------------- /themes/theme-material/all-debug.js: -------------------------------------------------------------------------------- 1 | import '../../ext/build/modern/theme-material/resources/theme-material-all-debug.css'; 2 | import './all.scss'; 3 | import '../../ext/build/modern/theme-material/theme-material-debug.js'; 4 | import '../../ext/build/modern/theme-material/resources/css-vars.js'; 5 | -------------------------------------------------------------------------------- /themes/theme-material/all.scss: -------------------------------------------------------------------------------- 1 | .myapp-dark-mode { 2 | background-color: grey; 3 | } -------------------------------------------------------------------------------- /webpack/common.js: -------------------------------------------------------------------------------- 1 | // webpack.common.js - common webpack config 2 | // node modules 3 | const path = require('path'); 4 | 5 | // webpack plugins 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | const WebpackNotifierPlugin = require('webpack-notifier'); 8 | 9 | // config files 10 | const pkg = require('../package.json'); 11 | const settings = require('./settings.js'); 12 | const { configureESBuildLoader } = require('./esbuild.config.js'); 13 | const { paths } = settings; 14 | 15 | // Configure Entries 16 | const configureEntries = () => { 17 | let entries = {}; 18 | for (const [key, value] of Object.entries(settings.entries)) { 19 | entries[key] = path.resolve(__dirname, settings.paths.src + value); 20 | } 21 | 22 | return entries; 23 | }; 24 | 25 | const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin'); 26 | 27 | // Common module exports 28 | // noinspection WebpackConfigHighlighting 29 | module.exports = (isProd) => { 30 | return { 31 | name: pkg.name, 32 | entry: configureEntries(), 33 | target: 'web', 34 | output: { 35 | path: path.resolve(__dirname, paths.dist.base), 36 | publicPath: './', 37 | }, 38 | plugins: [ 39 | new PreloadWebpackPlugin({ 40 | rel: 'preload', 41 | as: 'font', 42 | include: 'allAssets', 43 | fileWhitelist: [ 44 | /resources\/fonts(\/.*).(woff2?|ttf|woff)(\?.*)?$/i, 45 | ], 46 | fileBlacklist: [/codicon.ttf/], 47 | }), 48 | new WebpackNotifierPlugin({ 49 | title: 'Webpack', 50 | excludeWarnings: true, 51 | alwaysNotify: true, 52 | }), 53 | new CopyWebpackPlugin({ 54 | patterns: settings.copyFiles, 55 | }), 56 | ], 57 | resolve: { 58 | alias: { 59 | '@': path.resolve(__dirname, settings.paths.src), 60 | Ext: path.resolve( 61 | __dirname, 62 | `${paths.Ext}ext-modern-all${isProd ? '' : '-debug'}.js` 63 | ), 64 | }, 65 | fallback: { 66 | fs: false, 67 | }, 68 | }, 69 | module: { 70 | rules: [configureESBuildLoader()], 71 | }, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /webpack/css.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 2 | 3 | module.exports = { 4 | configureCssLoaders() { 5 | return [ 6 | { 7 | test: /\.((c|sa|sc)ss)$/i, 8 | exclude: [/ext\/build/], 9 | use: [ 10 | // fallback to style-loader in development 11 | process.env.NODE_ENV !== 'production' 12 | ? 'style-loader' 13 | : MiniCssExtractPlugin.loader, 14 | { 15 | loader: 'css-loader', 16 | options: { 17 | url: false, 18 | importLoaders: 1, 19 | sourceMap: true, 20 | modules: { 21 | auto: true, 22 | mode: 'local', 23 | exportGlobals: true, 24 | localIdentName: 25 | '[name]__[local]--[hash:base64:5]', 26 | }, 27 | }, 28 | }, 29 | { 30 | loader: 'sass-loader', 31 | options: { 32 | sourceMap: true, 33 | }, 34 | }, 35 | ], 36 | }, 37 | { 38 | test: /\.(css)$/, 39 | include: [/ext/], 40 | use: [ 41 | // fallback to style-loader in development 42 | process.env.NODE_ENV !== 'production' 43 | ? 'style-loader' 44 | : MiniCssExtractPlugin.loader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | url: true, 49 | }, 50 | }, 51 | ], 52 | }, 53 | { 54 | test: /\.(woff|woff2|svg|eot|ttf|otf)$/i, 55 | type: 'asset/resource', 56 | generator: { 57 | filename: 'resources/fonts/[name].[contenthash][ext]', 58 | publicPath: '../../', 59 | }, 60 | }, 61 | { 62 | test: /\.(png|jpg|jpeg|gif)$/i, 63 | type: 'asset/resource', 64 | generator: { 65 | filename: 'resources/images/[name].[contenthash][ext]', 66 | publicPath: '../../', 67 | }, 68 | }, 69 | ]; 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /webpack/dev.js: -------------------------------------------------------------------------------- 1 | // webpack.dev.js - developmental builds 2 | 3 | // node modules 4 | const { merge } = require('webpack-merge'); 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | // config files 10 | const commonConfig = require('./common.js'); 11 | const { devServerConfig } = require('./settings.js'); 12 | 13 | // Configure the webpack-dev-server 14 | const configureDevServer = () => { 15 | return { 16 | host: devServerConfig.host(), 17 | port: devServerConfig.port(), 18 | https: !!parseInt(devServerConfig.https()), 19 | hot: false, 20 | client: { 21 | overlay: { 22 | warnings: true, 23 | errors: true, 24 | }, 25 | }, 26 | liveReload: true, 27 | static: { 28 | directory: path.resolve(__dirname, '../'), 29 | publicPath: './', 30 | }, 31 | headers: { 32 | 'Access-Control-Allow-Origin': '*', 33 | }, 34 | 35 | // Use it to run a proxy 36 | // proxy: { 37 | // '/api': { 38 | // target: devServerConfig.remoteServerUrl, 39 | // ws: true, 40 | // secure: false, 41 | // cookieDomainRewrite: false, 42 | // cookiePathRewrite: { 43 | // '/path/': '/', 44 | // '/path': '/', 45 | // }, 46 | // debug: true, 47 | // changeOrigin: true, 48 | // onProxyRes: (proxyRes, request, response) => { 49 | // const setCookieHeader = proxyRes.headers['set-cookie']; 50 | // 51 | // //remove "Secure" option as such cookie will not be accepted when locally using HTTP 52 | // if (setCookieHeader && setCookieHeader.length > 0) { 53 | // let sessionCookie = setCookieHeader[0]; 54 | // 55 | // if (/secure/i.test(sessionCookie)) { 56 | // sessionCookie = sessionCookie.replace( 57 | // /\s*Secure;/i, 58 | // '' 59 | // ); 60 | // console.log( 61 | // `Rewriting set-cookie header to: ${sessionCookie}` 62 | // ); 63 | // proxyRes.headers['set-cookie'] = [sessionCookie]; 64 | // } 65 | // } 66 | // }, 67 | // } 68 | // }, 69 | }; 70 | }; 71 | 72 | const { configureCssLoaders } = require('./css.config.js'); 73 | 74 | console.log(process.env.NODE_ENV); 75 | 76 | // Development module exports 77 | module.exports = merge(commonConfig(false), { 78 | output: { 79 | filename: '[name].bundle.js', 80 | publicPath: 'auto', 81 | clean: true, 82 | }, 83 | mode: 'development', 84 | devtool: false, 85 | devServer: configureDevServer(), 86 | module: { 87 | rules: [...configureCssLoaders()], 88 | }, 89 | plugins: [ 90 | new webpack.DefinePlugin({ 91 | WEBPACK_APP_VERSION: JSON.stringify( 92 | require('../package.json').version 93 | ), 94 | }), 95 | new HtmlWebpackPlugin({ 96 | template: 'index.html', 97 | filename: 'index.html', 98 | inject: true, 99 | }), 100 | new webpack.HotModuleReplacementPlugin(), 101 | ], 102 | }); -------------------------------------------------------------------------------- /webpack/esbuild.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configureESBuildLoader() { 3 | return { 4 | test: /\.js$/, 5 | exclude: [ 6 | /(ext-)/, 7 | // \\ for Windows, / for macOS and Linux 8 | /node_modules[\\/]core-js/, 9 | /node_modules[\\/]webpack/, 10 | ], 11 | loader: 'esbuild-loader', 12 | options: { 13 | target: 'es2020', 14 | }, 15 | }; 16 | }, 17 | }; -------------------------------------------------------------------------------- /webpack/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | ], 5 | }; -------------------------------------------------------------------------------- /webpack/prod-server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const settings = require('./settings.js'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | devServer: { 7 | host: settings.devServerConfig.host(), 8 | static: [ 9 | // Simple example 10 | path.resolve(__dirname, settings.paths.dist.base), 11 | ], 12 | port: 9000, 13 | headers: { 14 | 'Access-Control-Allow-Origin': '*', 15 | }, 16 | // Use it for proxy 17 | // proxy: { 18 | // '/path/api': { 19 | // target: '', 20 | // ws: true, 21 | // secure: false, 22 | // cookieDomainRewrite: 'localhost', 23 | // debug: true, 24 | // }, 25 | // }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /webpack/prod.js: -------------------------------------------------------------------------------- 1 | // webpack.prod.js - production builds 2 | const webpack = require('webpack'); 3 | const git = require('git-rev-sync'); 4 | const { merge } = require('webpack-merge'); 5 | const moment = require('moment'); 6 | const path = require('path'); 7 | 8 | // webpack plugins 9 | //const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 10 | //const PurgecssPlugin = require('purgecss-webpack-plugin'); 11 | 12 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 13 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 14 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 15 | const zopfli = require('@gfx/zopfli'); 16 | //const CompressionPlugin = require('compression-webpack-plugin'); 17 | 18 | // config files 19 | const commonConfig = require('./common.js'); 20 | const settings = require('./settings.js'); 21 | 22 | // Configure file banner 23 | const configureBanner = () => { 24 | return { 25 | banner: [ 26 | '/*!', 27 | ' * @project ' + settings.name, 28 | ' * @name ' + '[base]', 29 | ' * @author ' + settings.copyright, 30 | ' * @build ' + moment().format('llll') + ' ET', 31 | ' * @release ' + git.long() + ' [' + git.branch() + ']', 32 | ' * @copyright Copyright (c) ' + 33 | moment().format('YYYY') + 34 | ' ' + 35 | settings.copyright, 36 | ' *', 37 | ' */', 38 | '', 39 | ].join('\n'), 40 | raw: true, 41 | include: [/bootstrap/], 42 | }; 43 | }; 44 | 45 | // Configure Compression webpack plugin 46 | const configureCompression = () => { 47 | return { 48 | filename: '[path].gz[query]', 49 | test: /\.(js|css|html|svg)$/, 50 | threshold: 10240, 51 | minRatio: 0.8, 52 | deleteOriginalAssets: false, 53 | compressionOptions: { 54 | numiterations: 15, 55 | level: 9, 56 | }, 57 | algorithm(input, compressionOptions, callback) { 58 | return zopfli.gzip(input, compressionOptions, callback); 59 | }, 60 | }; 61 | }; 62 | 63 | // Configure Clean webpack 64 | const configureCleanWebpack = () => { 65 | return { 66 | cleanOnceBeforeBuildPatterns: settings.paths.dist.clean, 67 | verbose: false, 68 | dry: false, 69 | }; 70 | }; 71 | 72 | const distPath = path.resolve(__dirname, settings.paths.dist.base); 73 | 74 | const { configureCssLoaders } = require('./css.config.js'); 75 | 76 | // Production module exports 77 | module.exports = [ 78 | merge(commonConfig(true), { 79 | mode: 'production', 80 | output: { 81 | path: distPath, 82 | filename: 'js/[name].[chunkhash].js', 83 | }, 84 | module: { 85 | rules: [ 86 | ...configureCssLoaders(), 87 | { 88 | test: /\.js$/, 89 | enforce: 'pre', 90 | exclude: /(node_modules|\.spec\.js)/, 91 | use: [ 92 | { 93 | loader: 'webpack-strip-block', 94 | options: { 95 | start: '', 96 | end: '', 97 | }, 98 | }, 99 | ], 100 | }, 101 | ], 102 | }, 103 | plugins: [ 104 | new webpack.BannerPlugin(configureBanner()), 105 | new webpack.DefinePlugin({ 106 | WEBPACK_APP_VERSION: JSON.stringify( 107 | require('../package.json').version 108 | ), 109 | }), 110 | new CleanWebpackPlugin(configureCleanWebpack()), 111 | new MiniCssExtractPlugin({ 112 | filename: 'resources/css/[name].[contenthash].css', 113 | experimentalUseImportModule: false, 114 | }), 115 | new HtmlWebpackPlugin({ 116 | template: 'templates/index.html', 117 | filename: 'server-index.html', 118 | inject: true, 119 | }), 120 | new HtmlWebpackPlugin({ 121 | template: 'index.html', 122 | filename: 'index.html', 123 | inject: true, 124 | }), 125 | // new webpack.SourceMapDevToolPlugin({ 126 | // filename: '[file].map[query]', 127 | // exclude: [/(Ext)/], 128 | // }), 129 | //new CompressionPlugin(configureCompression()), 130 | // new BundleAnalyzerPlugin({ 131 | // analyzerMode: 'static', 132 | // reportFilename: 'report-modern.html', 133 | // }), 134 | ], 135 | }), 136 | ]; 137 | -------------------------------------------------------------------------------- /webpack/settings.js: -------------------------------------------------------------------------------- 1 | // webpack.settings.js - webpack settings config 2 | 3 | require('dotenv').config(); 4 | 5 | // Webpack settings exports 6 | // noinspection WebpackConfigHighlighting 7 | module.exports = { 8 | name: 'App', 9 | copyright: 'Add copyright here', 10 | paths: { 11 | resources: '../resources', 12 | src: '../src/', 13 | Ext: '../ext/build/', 14 | dist: { 15 | base: '../build/', 16 | clean: ['**/*'], 17 | }, 18 | }, 19 | urls: { 20 | publicPath: './', 21 | }, 22 | entries: { 23 | bootstrap: 'bootstrap.js', 24 | }, 25 | copyFiles: [ 26 | { 27 | from: 'resources', 28 | to: 'resources', 29 | }, 30 | 31 | // Add your assets. Example: 32 | // { 33 | // from: './node_modules/@arcgis/core/assets', 34 | // to: 'resources/@arcgis/assets', 35 | // }, 36 | // { 37 | // from: './node_modules/monaco-editor/min/vs/base/browser/ui/codicons/codicon/', 38 | // to: 'resources/fonts/', 39 | // }, 40 | // { 41 | // from: './node_modules/@pdftron/webviewer/public', 42 | // to: './libs/pdftron', 43 | // }, 44 | ], 45 | devServerConfig: { 46 | host: () => process.env.DEVSERVER_HOST || 'localhost', 47 | poll: () => process.env.DEVSERVER_POLL || false, 48 | port: () => process.env.DEVSERVER_PORT || 8080, 49 | https: () => process.env.DEVSERVER_HTTPS || false, 50 | //remoteServerUrl: '', 51 | }, 52 | }; 53 | --------------------------------------------------------------------------------