@v1ggs/abstraction
v0.8.0
Published
Webpack simplified. An advanced configuration for differential serving and much more...
Downloads
1
Maintainers
Readme
- The Concept
- Basic Usage Info
- Requirements
- Features
- Getting Started
- Modes
- Config
- Polyfills
- Assets.json
- CMS
- Code Quality
- Useful Docs
- Changelog
- License
Still under development. New minor version may introduce breaking changes. See CHANGELOG.
The Concept
The main focus of this tool is simplicity of usage.
The config for Webpack and its loaders and plugins has been abstracted to include only as few (self-explanatory) options as possible. As a consequence, a detailed control over the build process is not available.
Support for frameworks like React is not available either.
If these are essential requirements, using another tool or creating a Webpack config manually is probably a better option.
How it works
Abstraction uses provided configuration (user's or default) to activate and configure required loaders and plugins, as well as Webpack itsef. Then it combines it all in one config and applies it to Webpack. Everything else is handled by Webpack.
It is based on Webpack 5, and you can use its features, like code splitting, dynamic imports with magic comments, tree shaking, hot module replacement...
Motivation
- Webpack's config is not simple.
- There are a lot of options for different loaders and plugins as well.
- A single option may require reading a lot of documentation.
- There are issues with some loaders and plugins, due to their incomplete documentation or for some other reasons.
- There may be compatibility issues between loaders and plugins.
- Sometimes it's just too difficult to find out why something doesn't work, even the docs say it should. :D
Finding solutions to all these issues may require a lot of time and work. This tool tries to eliminate that problem.
Basic Usage Info
Abstraction can work with the default configuration, just make sure your source dir is <project root>/src
and your entry file is <project root>/src/index.js
. You may need to add more entries. Working with a CMS has some requirements and needs a server configuration.
The default browserslist config is being used for transpilation, polyfilling and prefixing. If you need a different browser support, configure .browserslistrc
accordingly or delete it to build for differential serving (module/nomodule).
Bundles are transpiled with Babel (preset-env) and can be polyfilled automatically (default) or manually, with core-js. Other polyfills should be imported manually.
For now, it works with SCSS preprocessor. CSS is prefixed with autoprefixer. Unused selectors are removed from CSS by default.
Nunjucks is used for templates, a different loader can be configured. Templates are built automatically, they should't be imported in JavaScript. If you want to import them, prefix all template filenames with an "_".
In "differential serving" mode, you don't need two entry points for each bundle. JavaScript will produce two bundles (ES6 and ES5) for each entry point, and CSS will be prefixed to support all browsers. Dynamic imports are not suppoorted in differential serving mode.
NOTE:
- A basic knowledge of Webpack is requried.
Requirements
- Node.js
- NPM (comes with Node.js)
- (Optional) Editor extensions: ESLint, Stylelint and Prettier, for a better developer experience.
- A local backend server, if you're working with a CMS
Features
- Build and transpile JavaScript, based on
.browserslistrc
and automatically polyfill with core-js - Prefix CSS, remove unused selectors (logged), convert pixels to rems, sort media-queries, fix flexbox bugs
- Sourcemaps for JavaScript and CSS in development environment
- Live reload and hot module replacement
- Filenames with hashes for cachebusting in production
- Option to make a JavaScript polyfills bundle manually and conditionally import it where required
- Differential serving (module/nomodule) is configured automatically when you delete
.browserslistrc
- Code linting and formatting config (
.editorconfig
,.prettierrc.js
,.eslintrc
,.stylelintrc
) - Info about built files, together with additional information can be found in
assets.json
, for usage with other tools or a CMS - Work with templates (nunjucks by default), another loader can be added
- Create SVG sprites and load them automatically into HTML
- Optimise images
- Extract licenses and manually add those that are not being found during the build process
- Automatically restart the application to apply relevant config changes
- SSL for local development, if you have mkcert installed
- Develop for a CMS, with all webpack's features (a local backend domain required)
Getting Started
Install
npm i -D @v1ggs/abstraction
Prepare linters
# Set rules for Prettier, ESLint and StyleLint
npx abs-prepare
Prepare SSL
In order to use SSL (development only), you should have mkcert installed on your computer. Then create a certificate with:
npx abs-prepare-ssl
For a front-end project, a certificate will be created for localhost
. If you're working in a combination with a CMS, first configure server.backend
, then run the command. See Server section.
When you run the server, the certificate will be found and used automatically.
IMPORTANT:
- The certificate is created in
.cert
folder in the project's root. The folder should be automatically added to.gitignore
and.npmignore
, if they are found. Please check. If you use other "ignore" files, please add the folder manually.NOTE:
- Firefox does not recognise mkcert certificate as valid.
- If you don't use mkcert, you need to have an SSL certificate for
server.backend
.- If you have certificate for the backend domain, it can be used for the front-end. See Server section for an example how to use it.
- You may need to redirect your local backend domain to
127.0.0.0
or0.0.0.0
with hosts file (depends on your local server).
Run
# Run server in development mode
npx abs-run
# Build files in production mode (without serving)
npx abs-build
You may also need:
# Run server in production mode
npx abs-run-prod
# Build files in development mode (without serving)
npx abs-build-dev
NOTE:
- Some optimisations and actions are not performed in development, to improve performance:
- Assets are not optimised or minified.
- Filenames don't contain hashes (cache busting).
- License files are not produced.
- Bundle Analyser file is not produced.
- Many other Webpack's optimizations are performed only in production.
- Other differencies: sourcemaps are produced only in development.
- Development bundles contain a lot of runtime code, for Webpack and devServer to work properly.
- Falsey conditionals are not removed from code in development.
Modes
Abstraction has two working modes:
- Build bundles using
.browserslistrc
configuration for polyfilling and prefixing - Build two bundles for each entry, one with modern/es6 code, and the other with legacy/es5 code (module/nomodule)
Build with .browserslistrc
- Configure targeted browsers in
.browserslistrc
(optional) - Run Abstraction the way you want
Build for differential serving (module/nomodule)
- Delete
.browserslistrc
- Run Abstraction the way you want
In a front-end project, the bundles will be enqueued in HTML, so that you can test both modern and legacy bundles at the same time, one in a modern, the other in a legacy browser (even, now unsupported, IE11).
IMPORTANT:
- Dynamic imports are not supported in "differential serving" mode, but code splitting can be done with entry points, and SplitChunksPlugin will split common chunks.
NOTE:
- If you're using templates, this will work if your page has
<head></head>
section.- If you don't use templates, it's automatically configured.
- If you're working with a CMS, enqueue both
es5
andes6
bundles with PHP, using the information inassets.json
. Then usetype="module"
attribute fores6
andnomodule defer
fores5
.
Config
Create .abstraction.config.js
in the root of your project (you can find the default config here).
path = {}
Overrides the default src
and dist
directories.
.abstraction.config.js
module.exports = {
// ...
path: {
src: './src',
dist: './dist',
},
};
globals = {}
Defines global constants for usage in source code. Use the globals: {}
object to define them. There are default constants, that will work without configuration. Eexcept for DESIGN
and REM_SIZE
, they can't be overriden.
Example:
.abstraction.config.js
module.exports = {
// ...
globals: {
MY_GLOBAL_CONST: 'something',
},
};
Default constants:
module.exports = {
globals: {
PRODUCTION: true, // `false` in development.
PUBLIC_PATH: '/', // Autoconfigured: '/' | 'protocol://domain:port/'.
REM_SIZE: 16, // Configured with `css.baseFontSize`.
DESIGN: 'mobile-first', // Configured with `css.sortMQ`.
// These are not available in SCSS:
ENV_MAIN: true, // This is true in the modern bundle, false in the legacy.
ENV_LEGACY: true, // This is true in the legacy bundle, false in the modern.
},
};
In JavaScript and templates, you can use them the way they are declared. In SCSS, they are converted to the SCSS style: lowercased, prefixed with $
and underscores are converted to dashes.
Templates example (nunjucks):
{# Removes quotes from `PUBLIC_PATH` constant #}
{% set path = PUBLIC_PATH | replace('"', '') %}
<script src="{{ path }}/js/index.js"></script>
JavaScript example:
// src/index.js
// This will be removed completely in production (when minified).
if (!PRODUCTION) {
console.log('This is NOT production.');
}
SCSS example:
// src/index.scss
// Declared as `REM_SIZE: 16` in JS,
// converted to `$rem-size` in SCSS.
html {
font-size: $rem-size + px;
}
NOTE:
- The default globals are declared in
.eslintrc
, underglobals
. User defined globals have to be added manually.
javascript = {}
javascript.entry
(type:{ <entryChunkName> string | [string] } | {}
). Configures entry points. Takes the the original config.javascript.singleRuntimeChunk
(type:boolean
, defaultfalse
). Adds an additional chunk containing only the runtime to each entrypoint. If you have multiple entry points on a page, set it totrue
. Html-webpack-plugin will add all entries to HTML.javascript.polyfill
(type:string
, default:'auto'
, available:'auto'
,'manual'
). Polyfills bundles automatically. When'manual'
, polyfills have to be imported manually.javascript.providePlugin
(type:object
). Takes the original configuration. Useful if, for example, you want to use jQuery.
.abstraction.config.js
module.exports = {
// ...
javascript: {
entry: {
index: './src/index.js',
},
singleRuntimeChunk: false,
polyfill: 'auto',
providePlugin: {},
},
}
Configure supported browsers in .browserslistrc
. If you want to work the "module/nomodule" way, delete .browserslistrc
file.
Babel (preset-env) is being used for transpilation and core-js for automatic polyfilling.
Core-js version will be automatically determined an applied to the babel config, for the latest polyfills to be available for usage.
Terser will minify code in production
mode only.
NOTE:
- As a rule of thumb: Use exactly one entry point for each HTML document. See the issue described here for more details. Html-webpack-plugin will add all entries to HTML.
- Although using multiple entry points per page is allowed in webpack, it should be avoided when possible in favor of an entry point with multiple imports:
entry: { page: ['./analytics', './app'] }
. This results in a better optimization and consistent execution order when using async script tags.
css = {}
css.baseFontSize
(type:number
, default:16
). Use it to set the default font size for the project. It will be used to convertpx
torem
. Use it in source code asREM_SIZE
/$rem-size
constant. See Globals section.css.sortMQ
(type:string
, default:mobile-first
, available:mobile-first
,desktop-first
). Defines how media queries will be grouped and sorted.css.purge
(type:object
|false
). Setfalse
, to disable purging.css.purge.keepSelectors
(type:array
). Array of selectors that should stay in CSS under any circumstances. The selectors can start with.
or#
, but it does not have any effect, only the name matters (matched with RegEx).
Example:
.abstraction.config.js
module.exports = {
// ...
css: {
baseFontSize: 16,
sortMQ: 'desktop-first',
purge: {
keepSelectors: ['.selector-1', 'selector2'], // these will always stay in CSS
},
// purge: false, // Disable purging
},
};
Import your entry .scss
files into the corresponding JavaScript entries. For everything else (like @import
, @use
, fonts, images etc.) use SCSS the way you usually would. Paths should be relative to the .scss
file where they are being imported or refered to with url()
.
Example:
homepage.js
import './homepage.scss';
homepage.scss
@import './header/index'; // index.scss
.class {
background-image: url('./image.jpg'); // image in the same folder
// or
background-image: url('../some/path/to/image.webp'); // image from somewhere else
}
NOTE:
- CSS is purged by default, using purgecss. All removed selectors will be will be logged in a file, in
logs
folder in the project's root.- All templates and javascript are scanned and all found selectors will be kept.
- Dynamic selectors created with JavaScript will not be recognised by purgecss.
- If a CSS selector does not work in a page, purgecss may be the reason. Use
css.purge.keepSelectors
to keep it.
templates = {}
By default, simple-nunjucks-loader is used. Another loader can be configured.
templates.nunjucksOptions
(type:object
). Takes simple-nunjucks-loader's configuration.templates.customLoader
(type:object
). Configure another templates loader. BothfileTypes
anduse
have to be configured.templates.customLoader.fileTypes
(type:array
). Takes an array of file extensions (without a leading dot) that should be processed with the loader. It will be converted toRegEx
and used as the Webpack's Rule.testtemplates.customLoader.use
(type:object
). Takes the loader's config. This value is passed to the Webpack'sRule.use
, so you can add multiple loaders, if required.
Example for handlebars loader:
.abstraction.config.js
module.exports = {
// ...
templates: {
nunjucksOptions: {},
customLoader: {
fileTypes: ['hbs', 'handlebars'],
use: ['handlebars-loader'],
},
},
}
The source folder is resolved in nunjucks config. For a different loader, this has to be configured manually . This means that you can import
, extend
and include
templates and assets, relative to the first-level subfolders in src
.
For example, if you have this folder structure:
├── src
│ ├── components
│ │ ├── component1
│ │ │ ├── img
│ │ │ │ ├── image.jpg
│ │ │ │ ├── ...
│ │ │ ├── template-parts
│ │ │ │ ├── _header.njk
│ │ │ │ ├── ...
│ │ │ ├── _index.njk
│ │ ├── component2
│ │ │ ├── _index.njk
│ │ │ ├── ...
│ │ ├── ...
│ ├── homepage.njk
│ ...
In this example, you can include ./src/components/component1/template-parts/_header.njk
and ./src/component1/img/image.jpg
the same way in any template in the project.
Nunjucks example:
<!-- This will work in:
- src/homepage.njk
- src/components/component1/_index.njk
- src/components/component2/_index.njk
or any other file. -->
{% include "component1/template-parts/_header.njk" %}
<img src="{% static 'component1/img/image.jpg' %}">
IMPORTANT:
- Templates are built automatically, they should't be imported in JavaScript. If you want to import them, prefix all template filenames with an "_".
NOTE:
- Files whose name starts with an
_
(partials) and files in a folder whose name starts with an_
will not be built, unless they are imported in other templates.- If you don't use templates, a page will be automatically generated with
html-webpack-plugin
.
svg = {}
Config
Use svg: {}
object to configure SVG manipulation.
svg.extractFrom
(type:array
, default:['html', 'css']
, available:'html'
,'css'
,'js'
).
Extracts SVG from HTML, CSS and JavaScript. Files from CSS and JavaScript will be extracted into sprites. Files from HTML (e.g. <img src="{% static './svg/icon.svg' %}" alt="icon">
) will be extracted as separate files.
If not extracted, SVG will be bundled with JavaScript and url-encoded and inlined in CSS and HTML.
svg.optimize: {}
(type:object
).
Configures optimisation, activates or deactivates preset-default
's plugins.
SVG is optimised with SVGO and its preset-default. Available options/plugins are:
svg.optimize.removeTitle
(type:boolean
, default:true
). Removes the<title>
element from the document.
This option may have significant accessibility implications. See more here.
svg.optimize.removeDesc
(type:boolean
, default:true
). Removes the<desc>
element from the document.svg.optimize.removeComments
(type:boolean
, default:true
). Removes XML comments from the document.
By default, this option ignores legal comments, also known as "special comments" or "protected comments". See more here.
svg.optimize.removeMetadata
(type:boolean
, default:true
). Removes the<metadata>
element from the document.
There may be cases you'd want to disable this option, as some SVGs include copyright and licensing information in the metadata. See more here.
svg.optimize.cleanupIds
(type:boolean
, default:true
). Removes unused IDs, and minifys IDs that are referenced by other elements. See more here.svg.optimize.removeDoctype
(type:boolean
, default:true
). Removes the Document Type Definition, also known as the DOCTYPE, from the document.svg.optimize.removeViewBox
(type:boolean
, default:true
). Removes theviewBox
attribute where it matches the documents width and height.
This option prevents SVGs from scaling, so they will not fill their parent container, or may clip if the container is too small. See more here.
Example:
.abstraction.config.js
module.exports = {
// ...
svg: {
extractFrom: ['css'],
optimize: {
removeTitle: true,
removeDesc: true,
removeComments: true,
removeMetadata: true,
cleanupIds: true,
removeDoctype: true,
removeViewBox: true,
},
},
}
Usage
Templates:
SVG can be included in HTML with <img>
, <object>
or <iframe>
. They will be extracted as separate files, or url-encoded and inlined (see Config above). If you bundle SVG in JavaScript, they can be referenced just with their ID. See info below.
JavaScript:
Import SVG files in JavaScript.
Sprites bundled with JavaScript will be automatically injected into HTML, using svg-sprite-loader's browser runtime. Their filename, without extension, will be their ID. Refer to them in HTML with <svg><use xlink:href="#id"></use></svg>
.
Example:
icon.js
import icon from './svg/icon.svg';
page.html
<svg class="icon">
<use xlink:href="#icon"></use>
</svg>
If extracted from JavaScript into an external sprite, then the sprite has to be referenced in JavaScript.
Example:
import icon from './svg/icon.svg';
element.innerHTML = `<svg id="${icon.id}" class="icon" ${icon.viewBox}>
<!-- url contains id -->
<use xlink:href="${icon.url}"></use>
</svg>`;
CSS:
In SCSS use url()
.
SCSS example:
background-image: url("./svg/icon.svg");
/* will become: */
background-image: url(path/to/svg/sprite.svg#icon);
IMPORTANT:
- You may run into issues if the path to your project contains
#
,%
or similar simbols, e.g.C:\some\folder#name\project
.
images = {}
images.quality
(type:number
, default:80
). Optimisation level, from1
(low quality) to100
(high quality).
Example:
module.exports = {
// ...
images: {
quality: 80,
},
}
All images referenced in templates with <img>
, CSS with url()
and JavaScript with import
will be processed. Paths to them will be properly resolved.
Resizing is not supported. All required sizes should be created manually and referenced in source files.
server = {}
For a front-end project, you may not need any additional config. Webpack's devServer will serve files on http://localhost:8080
.
If you're working with a CMS, devServer will serve files on your local backend domain, on port 8080
. Make sure it's available.
You can completely override the default devServer config in server.devServer
object. It takes the original config.
An example:
.abstraction.config.js
module.exports = {
// ...
server: {
backend: 'https://your-backend-domain.local/',
// Override the default devServer config.
devServer: {
// Enable SSL and use a manually created SSL certificate.
https: {
ca: './path/to/server.pem',
pfx: './path/to/server.pfx',
key: './path/to/server.key',
cert: './path/to/server.crt',
passphrase: 'webpack-dev-server',
requestCert: true,
},
},
},
};
Polyfills
This section needs better documentation.
Build polyfills for manual usage
npx abs-build-polyfills
Assets.json
This file contains information about built assets, as well as some other info. This can be used with a CMS to enqueue scripts and styles that contain dynamic hashes in their filenames. When in serving mode, it can be found in the server's root, e.g. localhost:8080/assets.json
.
CMS
If you're working with a CMS, you need a local domain with a CMS installed.
In .abstraction.config.js
, set server.backend
to your local back-end domain. All Webpack's features will be available.
In development environment, enqueue built JavaScript files, for Webpack and devServer to work properly. Don't enqueuw CSS in development. Use assets.json
for all required info. The assets.json
file will be served by devServer, on the local backend domain (configured in server.backend
), on port 8080
(e.g. https://yourdomain.local:8080/assets.json
).
In production, enqueue CSS as well.
For a differential serving, enqueue both es5
and es6
bundles with PHP, reading information from assets.json
. Then use type="module"
attribute for es6
and nomodule defer
for es5
.
IMPORTANT:
- devServer will use the same domain as your backend. If you need your local backend to be on a certain port, avoid using
8080
, because it's reserved for devServer. You can also override the devServer port inserver.devServer
.
Code Quality
Formatting
You can use Prettier editor extension, to format code. You may want to edit .editorconfig
, .prettierrc.js
and .prettierignore
to fit your requirements.
- NOTE: Formatting rules have been removed from linters.
Linting
JavaScript
ESLint is integrated into Webpack, with eslint-webpack-plugin, so that the code is being linted during the build process.
You can install ESLint editor extension, to prevent mistakes before build.
- You may want to edit
.eslintrc
to fit your requirements.
SCSS
Stylelint is integrated into Webpack, with stylelint-webpack-plugin, so that the code is being linted during the build process.
You can use Stylelint editor extension, to prevent mistakes before build.
- You may want to edit
.stylelintrc
to fit your requirements.
Editor integration
.editorconfig
file configures editor and some of Prettier options.
If you're using editor extensions, then Prettier should be configured as the default code formatter.
- Formatting rules have been removed from linters.
This is an example for VSCode:
{
// ...
"prettier.enable": true,
// JS
"eslint.enable": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
},
// (S)CSS
// Disable vscode built-in validation.
"scss.validate": false,
"less.validate": false,
"css.validate": false,
// Use stylelint.
"stylelint.enable": true,
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
},
"stylelint.validate": [
"sass",
"scss",
"css"
],
"stylelint.snippet": [
"sass",
"scss",
"css"
],
}
Useful Docs
- Entry Points (Webpack)
- Dynamic Imports (Webpack)
- Prefetching/Preloading modules (Webpack)
- Lazy Loading (Webpack)
- Coloring SVGs in CSS Background Images