How To Setup Custom ESBuild with SCSS, PurgeCSS & LiveServer? - javascript

Background:
I have a Webpack setup that I use to preprocess SCSS with PurgeCSS with a live HMR server with esbuild-loader for speeding up compiles in Webpack but even then my compile times are still slow and I would like the raw-speed of ESBuild and remove Webpack setup altogether.
The basic setup of ESBuild is easy, you install esbuild using npm and add the following code in your package.json:
{
...
"scripts": {
...
"watch": "esbuild --bundle src/script.js --outfile=dist/script.js --watch"
},
...
}
and run it by using the following command:
npm run watch
This single-line configuration will bundle your scripts and styles (you can import style.css in script.js) and output the files in the dist directory but this doesn't allow advance configuration for ESBuild like outputting a different name for your stylesheet and script files or using plugins.
Problems:
How to configure ESBuild using an external config file?
ESBuild doesn't support SCSS out-of-the-box. How to configure external plugins like esbuild-sass-plugin and to go even further, how to setup PostCSS and its plugins like Autoprefixer?
How to setup dev server with auto-rebuild?
How to setup PurgeCSS?

Solutions:
1. How to configure ESBuild using an external config file?
Create a new file in root: esbuild.js with the following contents:
import esbuild from "esbuild";
esbuild
.build({
entryPoints: ["src/styles/style.css", "src/scripts/script.js"],
outdir: "dist",
bundle: true,
plugins: [],
})
.then(() => console.log("⚡ Build complete! ⚡"))
.catch(() => process.exit(1));
Add the following code in your package.json:
{
...
"scripts": {
...
"build": "node esbuild.js"
},
...
}
Run the build by using npm run build command and this would bundle up your stylesheets and scripts and output them in dist directory.
For more details and/or adding custom build options, please refer to ESBuild's Build API documentation.
2. ESBuild doesn't support SCSS out-of-the-box. How to configure external plugins like esbuild-sass-plugin and to go even further, how to setup PostCSS and plugins like Autoprefixer?
Install npm dependencies: npm i -D esbuild-sass-plugin postcss autoprefixer
Edit your esbuild.js to the following code:
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
// Generate CSS/JS Builds
esbuild
.build({
entryPoints: ["src/styles/style.scss", "src/scripts/script.js"],
outdir: "dist",
bundle: true,
metafile: true,
plugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(source);
return css;
},
}),
],
})
.then(() => console.log("⚡ Build complete! ⚡"))
.catch(() => process.exit(1));
3. How to setup dev server with auto-rebuild?
ESBuild has a limitation on this end, you can either pass in watch: true or run its server. It doesn't allow both.
ESBuild also has another limitation, it doesn't have HMR support like Webpack does.
So to live with both limitations and still allowing a server, we can use Live Server. Install it using npm i -D #compodoc/live-server.
Create a new file in root: esbuild_watch.js with the following contents:
import liveServer from '#compodoc/live-server';
import esbuild from 'esbuild';
import { sassPlugin } from 'esbuild-sass-plugin';
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
// Turn on LiveServer on http://localhost:7000
liveServer.start({
port: 7000,
host: 'localhost',
root: '',
open: true,
ignore: 'node_modules',
wait: 0,
});
// Generate CSS/JS Builds
esbuild
.build({
logLevel: 'debug',
metafile: true,
entryPoints: ['src/styles/style.scss', 'src/scripts/script.js'],
outdir: 'dist',
bundle: true,
watch: true,
plugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([autoprefixer]).process(
source
);
return css;
},
}),
],
})
.then(() => console.log('⚡ Styles & Scripts Compiled! ⚡ '))
.catch(() => process.exit(1));
Edit the scripts in your package.json:
{
...
"scripts": {
...
"build": "node esbuild.js",
"watch": "node esbuild_watch.js"
},
...
}
To run build use this command npm run build.
To run dev server with auto-rebuild run npm run watch. This is a "hacky" way to do things but does a fair-enough job.
4. How to setup PurgeCSS?
I found a great plugin for this: esbuild-plugin-purgecss by peteryuan but it wasn't allowing an option to be passed for the html/views paths that need to be parsed so I
created esbuild-plugin-purgecss-2 that does the job. To set it up, read below:
Install dependencies npm i -D esbuild-plugin-purgecss-2 glob-all.
Add the following code to your esbuild.js and esbuild_watch.js files:
// Import Dependencies
import glob from 'glob-all';
import purgecssPlugin2 from 'esbuild-plugin-purgecss-2';
esbuild
.build({
plugins: [
...
purgecssPlugin2({
content: glob.sync([
// Customize the following URLs to match your setup
'./*.html',
'./views/**/*.html'
]),
}),
],
})
...
Now running the npm run build or npm run watch will purgeCSS from the file paths mentioned in glob.sync([...] in the code above.
TL;DR:
Create an external config file in root esbuild.js and add the command to run it in package.json inside scripts: {..} e.g. "build": "node esbuild.js" to reference and run the config file by using npm run build.
ESBuild doesn't support HMR. Also, you can either watch or serve with ESBuild, not both. To overcome, use a separate dev server library like Live Server.
For the complete setup, please refer to my custom-esbuild-with-scss-purgecss-and-liveserver repository on github.
Final Notes:
I know this is a long thread but it took me a lot of time to figure these out. My intention is to have this here for others looking into the same problems and trying to figure out where to get started.
Thanks.

Adding to Arslan's terrific answer, you can use the PurgeCSS plug-in for postcss to totally eliminate Step 4.
First, install the postcss-purgecss package: npm install #fullhuman/postcss-purgecss
Then, replace the code from Step 2 in Arslan's answer with the code shown below (which eliminates the need for Step 4).
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";
import purgecss from "#fullhuman/postcss-purgecss";
// Generate CSS/JS Builds
esbuild
.build({
entryPoints: [
"roomflows/static/sass/project.scss",
"roomflows/static/js/project.js",
],
outdir: "dist",
bundle: true,
loader: {
".png": "dataurl",
".woff": "dataurl",
".woff2": "dataurl",
".eot": "dataurl",
".ttf": "dataurl",
".svg": "dataurl",
},
plugins: [
sassPlugin({
async transform(source) {
const { css } = await postcss([
purgecss({
content: ["roomflows/templates/**/*.html"],
}),
autoprefixer,
]).process(source, {
from: "roomflows/static/sass/project.scss",
});
return css;
},
}),
],
minify: true,
metafile: true,
sourcemap: true,
})
.then(() => console.log("⚡ Build complete! ⚡"))
.catch(() => process.exit(1));

Related

Run typescript build with tsconfig-paths using pm2

I am trying to run the build (.js files) of typescript with the tsconfig-paths in production, I have no problem running typescript with paths. Just when running the build on production with pm2.
I have tried:
apps: [
{
name: 'app',
script: './dist/index.js',
node_args: '-r ts-node/register -r tsconfig-paths/register',
},
],
TLDR: If as I assume you run info *the* common misunderstanding about tsconfig you may try:
{
apps: [
{
name: 'app',
script: './dist/index.js',
node_args: '-r ts-node/register -r tsconfig-paths/register',
env: {
"TS_NODE_BASEURL": "./dist"
}
},
}
Explanation:
Typescript allows us to specify path aliases so that we don'y have to use ugly relative paths like ../../../../config. To use this feature typically you would have a tsconfig.json like this:
...
"outDir": "./dist",
"baseUrl": "./src", /* if your code sits in the /src directory */
"paths": {
"#/*": ["*"]
},
...
Now you can do the following:
import config from "#/config";
It will compile without errors. During the compilation the requested modules are in the src directory. However:
$ node -r tsconfig-paths/register dist/index.js
Failure! Cannot find module '#/config'
Why is that? Because at runtime config no longer sits inside ./src but instead can be found in ./dist.
So how do we handle this?
Fortunately tsconfig-paths allows us to override baseUrl with TS_NODE_BASEURL env:
$ TS_NODE_BASEURL=./dist node -r tsconfig-paths/register dist/index.js
Success!

Svelte app shows blank page on first start

This my first time running a Svelte app and I have this issue where the app doesn't seem to know where build/build.css and build/build.js are.
I got the same issue when I tried Svelte with Tailwind.
This is my config when I created the project:
import svelte from 'rollup-plugin-svelte';
import commonjs from '#rollup/plugin-commonjs';
import resolve from '#rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
This is the ouput after running npm start:
Here is my folder structure:
What went wrong?
This looks very much like the official Svelte template. In this case, the command to build, watch, & serve is npm run dev.
npm start just runs the web server and serve existing files. You'd use it, for example to test your prod build after npm run build.

Webpack resolve alias and compile file under that alias

I have project which uses lerna ( monorepo, multiple packages ). Few of the packages are standalone apps.
What I want to achieve is having aliases on few of the packages to have something like dependency injection. So for example I have alias #package1/backendProvider/useCheckout and in webpack in my standalone app I resolve it as ../../API/REST/useCheckout . So when I change backend provider to something else I would only change it in webpack.
Problem
Problem appears when this alias is used by some other package ( not standalone app ). For example:
Directory structure looks like this:
Project
packageA
ComponentA
packageB
API
REST
useCheckout
standalone app
ComponentA is in packageA
useCheckout is in packageB under /API/REST/useCheckout path
ComponentA uses useCheckout with alias like import useCheckout from '#packageA/backendProvider/useCheckout
Standalone app uses componentA
The error I get is that Module not found: Can't resolve '#packageA/backendProvider/useCheckout
However when same alias is used in standalone app ( that has webpack with config provided below ) it is working. Problem occurs only for dependencies.
Potential solutions
I know that one solution would be to compile each package with webpack - but that doesn't really seem friendly. What I think is doable is to tell webpack to resolve those aliases to directory paths and then to recompile it. First part ( resolving aliases ) is done.
Current code
As I'm using NextJS my webpack config looks like this:
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Fixes npm packages that depend on `fs` module
config.node = {
fs: "empty"
};
const aliases = {
...
"#package1/backendProvider": "../../API/REST/"
};
Object.keys(aliases).forEach(alias => {
config.module.rules.push({
test: /\.(js|jsx)$/,
include: [path.resolve(__dirname, aliases[alias])],
use: [defaultLoaders.babel]
});
config.resolve.alias[alias] = path.resolve(__dirname, aliases[alias]);
});
return config;
}
You don’t need to use aliases. I have a similar setup, just switch to yarn (v1) workspaces which does a pretty smart trick, it adds sym link to all of your packages in the root node_modules.
This way, each package can import other packages without any issue.
In order to apply yarn workspaces with lerna:
// lerna.json
{
"npmClient": "yarn",
"useWorkspaces": true,
"packages": [
"packages/**"
],
}
// package.json
{
...
"private": true,
"workspaces": [
"packages/*",
]
...
}
This will enable yarn workspace with lerna.
The only think that remains to solve is to make consumer package to transpile the required package (since default configs of babel & webpack is to ignore node_module transpilation).
In Next.js project it is easy, use next-transpile-modules.
// next.config.js
const withTM = require('next-transpile-modules')(['somemodule', 'and-another']); // pass the modules you would like to see transpiled
module.exports = withTM();
In other packages that are using webpack you will need to instruct webpack to transpile your consumed packages (lets assume that they are under npm scope of #somescope/).
So for example, in order to transpile typescript, you can add additional module loader.
// webpack.config.js
{
...
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
include: /[\\/]node_modules[\\/]#somescope[\\/]/, // <-- instruct to transpile ts files from this path
options: {
allowTsInNodeModules: true, // <- this a specific option of ts-loader
transpileOnly: isDevelopment,
compilerOptions: {
module: 'commonjs',
noEmit: false,
},
},
}
]
}
...
resolve: {
symlinks: false, // <-- important
}
}
If you have css, you will need add a section for css as well.
Hope this helps.
Bonus advantage, yarn workspaces will reduce your node_modules size since it will install duplicate packages (with the same semver version) once!

Integrating Stylelint with Vue.js

I'm working on trying to integrate stylelint into my freshly created Vue project.
I thought this would be a simple case of using the Stylelint Webpack Plugin but when I run yarn serve, any errors completely freeze it with no output. If I run yarn build, the build will fail as intended but will only print "There was a stylelint error".
My vue.config.js is as follows:
const stylelintPlugin = require('stylelint-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new stylelintPlugin({ files: ['**/*.(vue|scss)'] }),
],
},
};
Here are my current versions from package.json:
"#vue/cli-service": "^3.9.0",
"stylelint": "^10.1.0",
"stylelint-config-recommended": "^2.2.0",
"stylelint-scss": "^3.9.2",
While this may come too late, here are working configurations by using stylelint-config-recommended-scss.
It is an extension to the 3rd party stylelint-scss plugin which needs to be installed along with stylelint itself. Also stylelint-webpack-plugin needs to be installed, which seems to have been missing from your setup.
Install dev dependencies:
# First remove an unnecessary one you had (with NPM):
npm uninstall stylelint-config-recommended
# Or with Yarn:
yarn remove stylelint-config-recommended
# Install dev deps with NPM:
npm i -D stylelint stylelint-scss stylelint-config-recommended-scss stylelint-webpack-plugin
# Or with Yarn:
yarn add -D stylelint stylelint-scss stylelint-config-recommended-scss stylelint-webpack-plugin
In your vue.config.js configuration file:
const StyleLintPlugin = require('stylelint-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new StyleLintPlugin({
files: ['src/**/*.{vue,scss}'],
}),
],
},
};
Create a file stylelint.config.js in your project's root folder:
module.exports = {
extends: 'stylelint-config-recommended-scss',
rules: {
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep']
}
]
}
};
In package.json you can add this lint:scss command (run by npm run lint:scss). It tries to run autofix on all rules, but please note that not all rules can be autofixed.
In that case the script will output a list of error lines and exit on error. You need to go and fix these by hand, and then re-run the script to see that the errors got fixed:
{
"scripts": {
"lint:scss": "stylelint ./src/**/*.{vue,scss} --fix"
}
}
Hope this helps! Please add a comment if I missed something.
my configuration is the same like ux.engineer written
but when i try run scripts npm run lint:scss then I have
node_modules/stylelint/node_modules/get-stdin/index.js:13
for await (const chunk of stdin) {
^^^^^
SyntaxError: Unexpected reserved word
it turned out that I had the wrong (old) node version, so pay attention for that

How can I remove bower from this project and use requirejs with yarn (noob)?

How can I switch this to avoid using bower?
I installed yeoman for the first time and the generator for knockoutjs use bower. Now I read bower support is limited and bootstrap use popper.js which in v2 will deprecate support for bower. I would like to avoid the headache now and learn at the same time.
RequireJS and every client side libraries is in /src/bower_modules.
If I install bootstrap using npm or yarn it will install them in /node_modules, which the browser doesn't have access.
Do I then use gulp to transfer the dist folder to my /src/bower_modules folder?
Folder structure:
/src/
|--bower_modules/
|--app/
|--require.config.js
/node_modules/
/gulpfile.js
gulpfile.js:
var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require.config.js') + '; require;'),
requireJsOptimizerConfig = merge(requireJsRuntimeConfig, {
out: 'scripts.js',
baseUrl: './src',
name: 'app/startup',
paths: {
requireLib: 'bower_modules/requirejs/require'
},
include: [
'requireLib',
'components/nav-bar/nav-bar',
'components/home-page/home',
'text!components/about-page/about.html'
],
insertRequire: ['app/startup'],
bundles: {
// If you want parts of the site to load on demand, remove them from the 'include' list
// above, and group them into bundles here.
// 'bundle-name': [ 'some/module', 'another/module' ],
// 'another-bundle-name': [ 'yet-another-module' ]
}
}),
transpilationConfig = {
root: 'src',
skip: ['bower_modules/**', 'app/require.config.js'],
babelConfig: {
modules: 'amd',
sourceMaps: 'inline'
}
},
babelIgnoreRegexes = transpilationConfig.skip.map(function(item) {
return babelCore.util.regexify(item);
});
app/require.config.js:
var require = {
baseUrl: ".",
paths: {
"bootstrap": "bower_modules/bootstrap/js/bootstrap.min",
"crossroads": "bower_modules/crossroads/dist/crossroads.min",
"hasher": "bower_modules/hasher/dist/js/hasher.min",
"popper": "bower_modules/popper.js/dist/popper",
"jquery": "bower_modules/jquery/dist/jquery",
"knockout": "bower_modules/knockout/dist/knockout",
"knockout-projections": "bower_modules/knockout-projections/dist/knockout-projections",
"signals": "bower_modules/js-signals/dist/signals.min",
"text": "bower_modules/requirejs-text/text"
},
shim: {
"bootstrap": { deps: ["popper", "jquery"] }
}
};
Sidenote: The origin of the issue is that I require popper for bootstrap and bootstrasp.bundle is not included in the bower version is seems. Also popper doesn't like bower very much and won't be supported very long. I also have multiple errors trying to include it. I would also like to learn the good way and since bower will not be around long I wouldn't mind not working with it at all.
Bower itself posted a blog about this recently: https://bower.io/blog/2017/how-to-migrate-away-from-bower/.
Here's the key takeaways:
Manually move any packages from bower.json to package.json that are available from both Bower and NPM
For any items that are only available via Bower you can use the bower-away NPM package to install those with NPM instead of Bower
Write a task runner script (Grunt/Gulp/etc...) to move the package(s) file(s) to your dist directory
Something like this should do it.
gulp.task('injectNpmPackages', function () {
return gulp.src([
path.join('/node_modules/my-package/build/my-package.min.js')
])
.pipe(gulp.dest('/dist/vendor/'));
});

Categories