Add support for Webpack as frontend pipeline

This commit is contained in:
Bruno Alla 2022-03-06 12:18:20 +00:00
parent b67b528636
commit db99d77010
26 changed files with 272 additions and 33 deletions

View File

@ -49,8 +49,12 @@ jobs:
script:
- name: Basic
args: ""
- name: Extended
args: "use_celery=y use_drf=y frontend_pipeline=Gulp"
- name: Celery & DRF
args: "use_celery=y use_drf=y"
- name: Gulp
args: "frontend_pipeline=Gulp"
- name: Webpack
args: "frontend_pipeline=Webpack"
name: "${{ matrix.script.name }} Docker"
runs-on: ubuntu-latest
@ -77,7 +81,9 @@ jobs:
- name: With Celery
args: "use_celery=y frontend_pipeline='Django Compressor'"
- name: With Gulp
args: "frontend_pipeline='Gulp'"
args: "frontend_pipeline=Gulp"
- name: With Webpack
args: "frontend_pipeline=Webpack"
name: "${{ matrix.script.name }} Bare metal"
runs-on: ubuntu-latest

View File

@ -45,7 +45,8 @@
"frontend_pipeline": [
"None",
"Django Compressor",
"Gulp"
"Gulp",
"Webpack"
],
"use_celery": "n",
"use_mailhog": "n",

View File

@ -109,10 +109,10 @@ Or add the DSN for your account, if you already have one:
.. _Sentry add-on: https://elements.heroku.com/addons/sentry
Gulp & Bootstrap compilation
++++++++++++++++++++++++++++
Gulp or Webpack
+++++++++++++++
If you've opted for Gulp, you'll most likely need to setup
If you've opted for Gulp or Webpack as frontend pipeline, you'll most likely need to setup
your app to use `multiple buildpacks`_: one for Python & one for Node.js:
.. code-block:: bash
@ -121,7 +121,7 @@ your app to use `multiple buildpacks`_: one for Python & one for Node.js:
At time of writing, this should do the trick: during deployment,
the Heroku should run ``npm install`` and then ``npm build``,
which runs Gulp in cookiecutter-django.
which run the SASS compilation & JS bundling.
If things don't work, please refer to the Heroku docs.

View File

@ -155,7 +155,7 @@ To run Celery locally, make sure redis-server is installed (instructions are ava
Sass Compilation & Live Reloading
---------------------------------
If you've opted for Gulp as front-end pipeline, the project comes configured with `Sass`_ compilation and `live reloading`_. As you change you Sass/JS source files, the task runner will automatically rebuild the corresponding CSS and JS assets and reload them in your browser without refreshing the page.
If you've opted for Gulp or Webpack as front-end pipeline, the project comes configured with `Sass`_ compilation and `live reloading`_. As you change you Sass/JS source files, the task runner will automatically rebuild the corresponding CSS and JS assets and reload them in your browser without refreshing the page.
#. Make sure that `Node.js`_ v16 is installed on your machine.
#. In the project root, install the JS dependencies with::

View File

@ -94,7 +94,10 @@ frontend_pipeline:
1. None
2. `Django Compressor`_
3. `Gulp`_: support Bootstrap recompilation with real-time variables alteration.
3. `Gulp`_
4. `Webpack`_
Both Gulp and Webpack support Bootstrap recompilation with real-time variables alteration.
use_celery:
Indicates whether the project should be configured to use Celery_.
@ -144,6 +147,7 @@ debug:
.. _PostgreSQL: https://www.postgresql.org/docs/
.. _Gulp: https://github.com/gulpjs/gulp
.. _Webpack: https://webpack.js.org
.. _AWS: https://aws.amazon.com/s3/
.. _GCP: https://cloud.google.com/storage/

View File

@ -10,6 +10,7 @@ TODO: restrict Cookiecutter Django project initialization to
"""
from __future__ import print_function
import json
import os
import random
import shutil
@ -98,12 +99,90 @@ def remove_sass_files():
shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "static", "sass"))
def remove_webpack_files():
shutil.rmtree("webpack")
remove_vendors_js()
def remove_vendors_js():
vendors_js_path = os.path.join(
"{{ cookiecutter.project_slug }}",
"static",
"js",
"vendors.js",
)
if os.path.exists(vendors_js_path):
os.remove(vendors_js_path)
def remove_packagejson_file():
file_names = ["package.json"]
for file_name in file_names:
os.remove(file_name)
def update_package_json(remove_dev_deps=None, remove_keys=None, scripts=None):
remove_dev_deps = remove_dev_deps or []
remove_keys = remove_keys or []
scripts = scripts or {}
with open("package.json", mode="r") as fd:
content = json.load(fd)
for package_name in remove_dev_deps:
content["devDependencies"].pop(package_name)
for key in remove_keys:
content.pop(key)
content["scripts"].update(scripts)
with open("package.json", mode="w") as fd:
json.dump(content, fd, ensure_ascii=False, indent=2)
fd.write("\n")
def handle_js_runner(choice):
if choice == "Gulp":
update_package_json(
remove_dev_deps=[
"@babel/core",
"@babel/preset-env",
"babel-loader",
"css-loader",
"mini-css-extract-plugin",
"postcss-loader",
"postcss-preset-env",
"sass-loader",
"webpack",
"webpack-bundle-tracker",
"webpack-cli",
"webpack-dev-server",
"webpack-merge",
],
remove_keys=["babel"],
scripts={
"dev": "gulp",
"build": "gulp generate-assets",
},
)
remove_webpack_files()
elif choice == "Webpack":
update_package_json(
remove_dev_deps=[
"browser-sync",
"cssnano",
"gulp",
"gulp-imagemin",
"gulp-plumber",
"gulp-postcss",
"gulp-rename",
"gulp-sass",
"gulp-uglify-es",
],
scripts={
"dev": "webpack serve --config webpack/dev.config.js ",
"build": "webpack --config webpack/prod.config.js",
},
)
remove_gulp_files()
def remove_celery_files():
file_names = [
os.path.join("config", "celery_app.py"),
@ -383,13 +462,16 @@ def main():
if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
append_to_gitignore_file("!.envs/.local/")
if "{{ cookiecutter.frontend_pipeline }}" != "Gulp":
if "{{ cookiecutter.frontend_pipeline }}".lower() in ["none", "django compressor"]:
remove_gulp_files()
remove_webpack_files()
remove_packagejson_file()
if "{{ cookiecutter.use_docker }}".lower() == "y":
remove_node_dockerfile()
else:
handle_js_runner("{{ cookiecutter.frontend_pipeline }}")
if "{{ cookiecutter.cloud_provider}}" == "None":
if "{{ cookiecutter.cloud_provider }}" == "None":
print(
WARNING + "You chose not to use a cloud provider, "
"media files won't be served in production." + TERMINATOR

View File

@ -32,13 +32,11 @@ pytest
# Make sure the check doesn't raise any warnings
python manage.py check --fail-level WARNING
# Run npm build script if package.json is present
if [ -f "package.json" ]
then
npm install
if [ -f "gulpfile.js" ]
then
npm run build
fi
npm run build
fi
# Generate the HTML for the documentation

View File

@ -90,6 +90,7 @@ SUPPORTED_COMBINATIONS = [
{"frontend_pipeline": "None"},
{"frontend_pipeline": "django-compressor"},
{"frontend_pipeline": "Gulp"},
{"frontend_pipeline": "Webpack"},
{"use_celery": "y"},
{"use_celery": "n"},
{"use_mailhog": "y"},

View File

@ -41,3 +41,9 @@ docker-compose -f local.yml run django python manage.py check --fail-level WARNI
# Generate the HTML for the documentation
docker-compose -f local.yml run docs make html
# Run npm build script if package.json is present
if [ -f "package.json" ]
then
docker-compose -f local.yml run node npm run build
fi

View File

@ -344,3 +344,7 @@ project.min.css
vendors.js
*.min.js
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
{{ cookiecutter.project_slug }}/static/webpack_bundles/
webpack-stats.json
{%- endif %}

View File

@ -10,7 +10,7 @@
<option value="celeryworker"/>
<option value="celerybeat"/>
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Gulp' %}
{%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
<option value="node"/>
{%- endif %}
</list>

View File

@ -13,7 +13,7 @@
</facet>
</component>
<component name="NewModuleRootManager">
{% if cookiecutter.frontend_pipeline == 'Gulp' %}
{% if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
</content>

View File

@ -1,6 +1,6 @@
ARG PYTHON_VERSION=3.9-slim-bullseye
{% if cookiecutter.frontend_pipeline == 'Gulp' -%}
{% if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] -%}
FROM node:16-bullseye-slim as client-builder
ARG APP_HOME=/app
@ -99,7 +99,7 @@ RUN chmod +x /start-flower
# copy application code to WORKDIR
{%- if cookiecutter.frontend_pipeline == 'Gulp' %}
{%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
COPY --from=client-builder --chown=django:django ${APP_HOME} ${APP_HOME}
{% else %}
COPY --chown=django:django . ${APP_HOME}

View File

@ -89,6 +89,9 @@ THIRD_PARTY_APPS = [
"corsheaders",
"drf_spectacular",
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
"webpack_loader",
{%- endif %}
]
LOCAL_APPS = [
@ -351,6 +354,19 @@ SPECTACULAR_SETTINGS = {
{"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"},
],
}
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
# django-webpack-loader
# ------------------------------------------------------------------------------
WEBPACK_LOADER = {
"DEFAULT": {
"CACHE": not DEBUG,
"STATS_FILE": ROOT_DIR / "webpack-stats.json",
"POLL_INTERVAL": 0.1,
"IGNORE": [r".+\.hot-update.js", r".+\.map"],
}
}
{%- endif %}
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -69,7 +69,7 @@ if env("USE_DOCKER") == "yes":
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
{%- if cookiecutter.frontend_pipeline == 'Gulp' %}
{%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
try:
_, _, ips = socket.gethostbyname_ex("node")
INTERNAL_IPS.extend(ips)
@ -94,6 +94,12 @@ CELERY_TASK_ALWAYS_EAGER = True
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates
CELERY_TASK_EAGER_PROPAGATES = True
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
# django-webpack-loader
# ------------------------------------------------------------------------------
WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG # noqa F405
{%- endif %}
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -10,6 +10,7 @@ const pjson = require('./package.json')
const autoprefixer = require('autoprefixer')
const browserSync = require('browser-sync').create()
const concat = require('gulp-concat')
const tildeImporter = require('node-sass-tilde-importer');
const cssnano = require ('cssnano')
const imagemin = require('gulp-imagemin')
const pixrem = require('pixrem')
@ -27,7 +28,6 @@ function pathsConfig(appName) {
const vendorsRoot = 'node_modules'
return {
bootstrapSass: `${vendorsRoot}/bootstrap/scss`,
vendorsJs: [
`${vendorsRoot}/@popperjs/core/dist/umd/popper.js`,
`${vendorsRoot}/bootstrap/dist/js/bootstrap.js`,
@ -61,8 +61,8 @@ function styles() {
return src(`${paths.sass}/project.scss`)
.pipe(sass({
importer: tildeImporter,
includePaths: [
paths.bootstrapSass,
paths.sass
]
}).on('error', sass.logError))

View File

@ -107,7 +107,7 @@ services:
command: /start-flower
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Gulp' %}
{%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
node:
build:

View File

@ -1,13 +1,16 @@
{
"name": "{{cookiecutter.project_slug}}",
"version": "{{ cookiecutter.version }}",
"dependencies": {},
"devDependencies": {
"bootstrap": "^5.1.3",
"gulp-concat": "^2.6.1",
"@babel/core": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@popperjs/core": "^2.10.2",
"autoprefixer": "^10.4.0",
"babel-loader": "^8.2.3",
"bootstrap": "^5.1.3",
"browser-sync": "^2.27.7",
"css-loader": "^6.5.1",
"gulp-concat": "^2.6.1",
"cssnano": "^5.0.11",
"gulp": "^4.0.2",
"gulp-imagemin": "^7.1.0",
@ -16,9 +19,19 @@
"gulp-rename": "^2.0.0",
"gulp-sass": "^5.0.0",
"gulp-uglify-es": "^3.0.0",
"mini-css-extract-plugin": "^2.4.5",
"node-sass-tilde-importer": "^1.0.2",
"pixrem": "^5.0.0",
"postcss": "^8.3.11",
"sass": "^1.43.4"
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.0.2",
"sass": "^1.43.4",
"sass-loader": "^12.4.0",
"webpack": "^5.65.0",
"webpack-bundle-tracker": "^1.4.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0",
"webpack-merge": "^5.8.0"
},
"engines": {
"node": "16"
@ -26,8 +39,11 @@
"browserslist": [
"last 2 versions"
],
"babel": {
"presets": ["@babel/preset-env"]
},
"scripts": {
"dev": "gulp",
"build": "gulp generate-assets"
"dev": "",
"build": ""
}
}

View File

@ -46,3 +46,6 @@ django-cors-headers==3.11.0 # https://github.com/adamchainz/django-cors-headers
# DRF-spectacular for api documentation
drf-spectacular==0.21.2 # https://github.com/tfranzel/drf-spectacular
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
django-webpack-loader==1.4.1 # https://github.com/django-webpack/django-webpack-loader
{%- endif %}

View File

@ -0,0 +1,55 @@
const path = require('path');
const BundleTracker = require('webpack-bundle-tracker');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
target: "web",
context: path.join(__dirname, '../'),
entry: {
'project': path.resolve(__dirname, '../{{cookiecutter.project_slug}}/static/js/project'),
'vendors': path.resolve(__dirname, '../{{cookiecutter.project_slug}}/static/js/vendors'),
},
output: {
path: path.resolve(__dirname, '../{{cookiecutter.project_slug}}/static/webpack_bundles/'),
publicPath: '/static/webpack_bundles/',
filename: 'js/[name]-[fullhash].js',
chunkFilename: 'js/[name]-[hash].js'
},
plugins: [
new BundleTracker({filename: path.resolve(__dirname, '../webpack-stats.json')}),
new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css' }),
],
module: {
rules: [
// we pass the output from babel loader to react-hot loader
{
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.s?css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env',
'autoprefixer',
'pixrem',
],
},
},
},
'sass-loader',
],
}
],
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.jsx']
},
}

View File

@ -0,0 +1,16 @@
const { merge } = require('webpack-merge');
const commonConfig = require('./common.config');
module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
port: 3000,
proxy: {
'/': 'http://0.0.0.0:8000',
},
// We need hot=false (Disable HMR) to set liveReload=true
hot: false,
liveReload: true,
},
})

View File

@ -0,0 +1,8 @@
const { merge } = require('webpack-merge');
const devConfig = require('./common.config');
module.exports = merge(devConfig, {
mode: 'production',
devtool: 'source-map',
bail: true,
})

View File

@ -1 +1,5 @@
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
import '../sass/project.scss';
{%- endif %}
/* Project specific Javascript goes here. */

View File

@ -0,0 +1,2 @@
import '@popperjs/core';
import 'bootstrap';

View File

@ -1,5 +1,5 @@
@import "custom_bootstrap_vars";
@import "bootstrap";
@import "~bootstrap/scss/bootstrap";
// project specific CSS goes here

View File

@ -1,4 +1,8 @@
{% raw %}{% load static i18n {% endraw %}{% if cookiecutter.frontend_pipeline == 'Django Compressor' %}compress{% endif %}{% raw %}%}<!DOCTYPE html>
{% raw %}{% load static i18n {% endraw %}
{%- if cookiecutter.frontend_pipeline == 'Django Compressor' %}compress
{%- endif %}{% raw %}%}{% endraw %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}{% raw %}{% load render_bundle from webpack_loader %}{% endraw %}
{%- endif %}{% raw %}<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
@ -31,6 +35,8 @@
{% endcompress %}
{%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %}
<link href="{% static 'css/project.min.css' %}" rel="stylesheet">
{%- endraw %}{% elif cookiecutter.frontend_pipeline == "Webpack" %}{% raw %}
{% render_bundle 'project' 'css' %}
{%- endraw %}{% endif %}{% raw %}
{% endblock %}
<!-- Le javascript
@ -38,8 +44,11 @@
{# Placed at the top of the document so pages load faster with defer #}
{% block javascript %}
{%- endraw %}{% if cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %}
<!-- Vendor dependencies bundled as one file-->
<!-- Vendor dependencies bundled as one file -->
<script defer src="{% static 'js/vendors.min.js' %}"></script>
{%- endraw %}{% elif cookiecutter.frontend_pipeline == "Webpack" %}{% raw %}
<!-- Vendor dependencies bundled as one file -->
{% render_bundle 'vendors' 'js' attrs='defer' %}
{%- endraw %}{% else %}{% raw %}
<!-- Bootstrap JS -->
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.min.js" integrity="sha512-OvBgP9A2JBgiRad/mM36mkzXSXaJE9BEIENnVEmeZdITvwT09xnxLtT4twkCa8m/loMbPHsvPl0T8lRGVBwjlQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
@ -55,6 +64,8 @@
{% endcompress %}
{%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %}
<script defer src="{% static 'js/project.min.js' %}"></script>
{%- endraw %}{% elif cookiecutter.frontend_pipeline == "Webpack" %}{% raw %}
{% render_bundle 'project' 'js' attrs='defer' %}
{%- endraw %}{% endif %}{% raw %}
{% endblock javascript %}