Add Webpack support (#3623)

* Add support for Webpack as frontend pipeline

* Rename CI jobs

* Fix a couple of issues with Webpack + Docker

* Don't include Boostrap CSS from CDN with Webpack

* Rename variable

* Set publicPath in prod webpack config

* Fix removal of SASS files in post-gen hooks

* Add Webpack to readme usage section

* Run Django + Webpack dev server concurrently without Docker

* Fix async runserver command with Gulp/Webpack

* Upgrade django-webpack-loader to 1.5.0

* Pass variables required by Webpack at build time

* Upgrade django-webpack-loader to 1.7.0

* Add missing condition

* Add support for Azure Storage + Webpack

* Whitespaces

* Rename ROOT_DIR -> BASE_DIR

* Rename jobs

* Bump django-webpack-loader to latest

* Document limitation of Docker + Webpack + no Whitenoise

* Update section on custom Bootstrap compilation in generated readme
This commit is contained in:
Bruno Alla 2023-01-29 12:12:12 +00:00 committed by GitHub
parent 977ffd91a3
commit cbb0e19de7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 399 additions and 49 deletions

View File

@ -29,7 +29,7 @@ jobs:
- windows-latest - windows-latest
- macOS-latest - macOS-latest
name: "Run tests" name: "pytest ${{ matrix.os }}"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -49,10 +49,14 @@ jobs:
script: script:
- name: Basic - name: Basic
args: "" args: ""
- name: Extended - name: Celery & DRF
args: "use_celery=y use_drf=y frontend_pipeline=Gulp" args: "use_celery=y use_drf=y"
- name: Gulp
args: "frontend_pipeline=Gulp"
- name: Webpack
args: "frontend_pipeline=Webpack"
name: "${{ matrix.script.name }} Docker" name: "Docker ${{ matrix.script.name }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
@ -74,12 +78,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
script: script:
- name: With Celery - name: Celery
args: "use_celery=y frontend_pipeline='Django Compressor'" args: "use_celery=y frontend_pipeline='Django Compressor'"
- name: With Gulp - name: Gulp
args: "frontend_pipeline='Gulp'" args: "frontend_pipeline=Gulp"
- name: Webpack
args: "frontend_pipeline=Webpack"
name: "${{ matrix.script.name }} Bare metal" name: "Bare metal ${{ matrix.script.name }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
redis: redis:

View File

@ -27,7 +27,7 @@ production-ready Django projects quickly.
- Registration via [django-allauth](https://github.com/pennersr/django-allauth) - Registration via [django-allauth](https://github.com/pennersr/django-allauth)
- Comes with custom user model ready to go - Comes with custom user model ready to go
- Optional basic ASGI setup for Websockets - Optional basic ASGI setup for Websockets
- Optional custom static build using Gulp and livereload - Optional custom static build using Gulp or Webpack
- Send emails via [Anymail](https://github.com/anymail/django-anymail) (using [Mailgun](http://www.mailgun.com/) by default or Amazon SES if AWS is selected cloud provider, but switchable) - Send emails via [Anymail](https://github.com/anymail/django-anymail) (using [Mailgun](http://www.mailgun.com/) by default or Amazon SES if AWS is selected cloud provider, but switchable)
- Media storage using Amazon S3, Google Cloud Storage or Azure Storage - Media storage using Amazon S3, Google Cloud Storage or Azure Storage
- Docker support using [docker-compose](https://github.com/docker/compose) for development and production (using [Traefik](https://traefik.io/) with [LetsEncrypt](https://letsencrypt.org/) support) - Docker support using [docker-compose](https://github.com/docker/compose) for development and production (using [Traefik](https://traefik.io/) with [LetsEncrypt](https://letsencrypt.org/) support)
@ -149,6 +149,7 @@ Answer the prompts with your own desired [options](http://cookiecutter-django.re
1 - None 1 - None
2 - Django Compressor 2 - Django Compressor
3 - Gulp 3 - Gulp
4 - Webpack
Choose from 1, 2, 3, 4 [1]: 1 Choose from 1, 2, 3, 4 [1]: 1
use_celery [n]: y use_celery [n]: y
use_mailhog [n]: n use_mailhog [n]: n

View File

@ -46,7 +46,8 @@
"frontend_pipeline": [ "frontend_pipeline": [
"None", "None",
"Django Compressor", "Django Compressor",
"Gulp" "Gulp",
"Webpack"
], ],
"use_celery": "n", "use_celery": "n",
"use_mailhog": "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 .. _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: your app to use `multiple buildpacks`_: one for Python & one for Node.js:
.. code-block:: bash .. 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, At time of writing, this should do the trick: during deployment,
the Heroku should run ``npm install`` and then ``npm build``, 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. If things don't work, please refer to the Heroku docs.

View File

@ -84,6 +84,32 @@ You can read more about this feature and how to configure it, at `Automatic HTTP
.. _Automatic HTTPS: https://docs.traefik.io/https/acme/ .. _Automatic HTTPS: https://docs.traefik.io/https/acme/
.. _webpack-whitenoise-limitation:
Webpack without Whitenoise limitation
-------------------------------------
If you opt for Webpack without Whitenoise, Webpack needs to know the static URL at build time, when running ``docker-compose build`` (See ``webpack/prod.config.js``). Depending on your setup, this URL may come from the following environment variables:
- ``AWS_STORAGE_BUCKET_NAME``
- ``DJANGO_AWS_S3_CUSTOM_DOMAIN``
- ``DJANGO_GCP_STORAGE_BUCKET_NAME``
- ``DJANGO_AZURE_CONTAINER_NAME``
The Django settings are getting these values at runtime via the ``.envs/.production/.django`` file , but Docker does not read this file at build time, it only look for a ``.env`` in the root of the project. Failing to pass the values correctly will result in a page without CSS styles nor javascript.
To solve this, you can either:
1. merge all the env files into ``.env`` by running::
merge_production_dotenvs_in_dotenv.py
2. create a ``.env`` file in the root of the project with just variables you need. You'll need to also define them in ``.envs/.production/.django`` (hence duplicating them).
3. set these variables when running the build command::
DJANGO_AWS_S3_CUSTOM_DOMAIN=example.com docker-compose -f production.yml build``.
None of these options are ideal, we're open to suggestions on how to improve this. If you think you have one, please open an issue or a pull request.
(Optional) Postgres Data Volume Modifications (Optional) Postgres Data Volume Modifications
--------------------------------------------- ---------------------------------------------

View File

@ -172,7 +172,7 @@ You can also use Django admin to queue up tasks, thanks to the `django-celerybea
Sass Compilation & Live Reloading 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. #. Make sure that `Node.js`_ v16 is installed on your machine.
#. In the project root, install the JS dependencies with:: #. In the project root, install the JS dependencies with::

View File

@ -95,7 +95,10 @@ frontend_pipeline:
1. None 1. None
2. `Django Compressor`_ 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: use_celery:
Indicates whether the project should be configured to use Celery_. Indicates whether the project should be configured to use Celery_.
@ -145,6 +148,7 @@ debug:
.. _PostgreSQL: https://www.postgresql.org/docs/ .. _PostgreSQL: https://www.postgresql.org/docs/
.. _Gulp: https://github.com/gulpjs/gulp .. _Gulp: https://github.com/gulpjs/gulp
.. _Webpack: https://webpack.js.org
.. _AWS: https://aws.amazon.com/s3/ .. _AWS: https://aws.amazon.com/s3/
.. _GCP: https://cloud.google.com/storage/ .. _GCP: https://cloud.google.com/storage/

View File

@ -1,5 +1,5 @@
Troubleshooting Troubleshooting
===================================== ===============
This page contains some advice about errors and problems commonly encountered during the development of Cookiecutter Django applications. This page contains some advice about errors and problems commonly encountered during the development of Cookiecutter Django applications.
@ -38,6 +38,16 @@ To fix this, you can either:
.. _rm: https://docs.docker.com/engine/reference/commandline/volume_rm/ .. _rm: https://docs.docker.com/engine/reference/commandline/volume_rm/
.. _prune: https://docs.docker.com/v17.09/engine/reference/commandline/system_prune/ .. _prune: https://docs.docker.com/v17.09/engine/reference/commandline/system_prune/
Variable is not set. Defaulting to a blank string
-------------------------------------------------
Example::
WARN[0000] The "DJANGO_AWS_STORAGE_BUCKET_NAME" variable is not set. Defaulting to a blank string.
WARN[0000] The "DJANGO_AWS_S3_CUSTOM_DOMAIN" variable is not set. Defaulting to a blank string.
You have probably opted for Docker + Webpack without Whitenoise. This is a know limitation of the combination, which needs a little bit of manual intervention. See the :ref:`dedicated section about it <webpack-whitenoise-limitation>`.
Others Others
------ ------

View File

@ -10,6 +10,7 @@ TODO: restrict Cookiecutter Django project initialization to
""" """
from __future__ import print_function from __future__ import print_function
import json
import os import os
import random import random
import shutil import shutil
@ -87,15 +88,30 @@ def remove_heroku_build_hooks():
shutil.rmtree("bin") shutil.rmtree("bin")
def remove_sass_files():
shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "static", "sass"))
def remove_gulp_files(): def remove_gulp_files():
file_names = ["gulpfile.js"] file_names = ["gulpfile.js"]
for file_name in file_names: for file_name in file_names:
os.remove(file_name) os.remove(file_name)
remove_sass_files()
def remove_sass_files(): def remove_webpack_files():
shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "static", "sass")) 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(): def remove_packagejson_file():
@ -104,6 +120,83 @@ def remove_packagejson_file():
os.remove(file_name) 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, use_docker, use_async):
if choice == "Gulp":
update_package_json(
remove_dev_deps=[
"@babel/core",
"@babel/preset-env",
"babel-loader",
"concurrently",
"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":
scripts = {
"dev": "webpack serve --config webpack/dev.config.js",
"build": "webpack --config webpack/prod.config.js",
}
remove_dev_deps = [
"browser-sync",
"cssnano",
"gulp",
"gulp-imagemin",
"gulp-plumber",
"gulp-postcss",
"gulp-rename",
"gulp-sass",
"gulp-uglify-es",
]
if not use_docker:
dev_django_cmd = (
"uvicorn config.asgi:application --reload"
if use_async
else "python manage.py runserver_plus"
)
scripts.update(
{
"dev": "concurrently npm:dev:*",
"dev:webpack": "webpack serve --config webpack/dev.config.js",
"dev:django": dev_django_cmd,
}
)
else:
remove_dev_deps.append("concurrently")
update_package_json(remove_dev_deps=remove_dev_deps, scripts=scripts)
remove_gulp_files()
def remove_celery_files(): def remove_celery_files():
file_names = [ file_names = [
os.path.join("config", "celery_app.py"), os.path.join("config", "celery_app.py"),
@ -384,13 +477,21 @@ def main():
if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y": if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y":
append_to_gitignore_file("!.envs/.local/") append_to_gitignore_file("!.envs/.local/")
if "{{ cookiecutter.frontend_pipeline }}" != "Gulp": if "{{ cookiecutter.frontend_pipeline }}" in ["None", "Django Compressor"]:
remove_gulp_files() remove_gulp_files()
remove_webpack_files()
remove_sass_files()
remove_packagejson_file() remove_packagejson_file()
if "{{ cookiecutter.use_docker }}".lower() == "y": if "{{ cookiecutter.use_docker }}".lower() == "y":
remove_node_dockerfile() remove_node_dockerfile()
else:
handle_js_runner(
"{{ cookiecutter.frontend_pipeline }}",
use_docker=("{{ cookiecutter.use_docker }}".lower() == "y"),
use_async=("{{ cookiecutter.use_async }}".lower() == "y"),
)
if "{{ cookiecutter.cloud_provider}}" == "None": if "{{ cookiecutter.cloud_provider }}" == "None":
print( print(
WARNING + "You chose not to use a cloud provider, " WARNING + "You chose not to use a cloud provider, "
"media files won't be served in production." + TERMINATOR "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 # Make sure the check doesn't raise any warnings
python manage.py check --fail-level WARNING python manage.py check --fail-level WARNING
# Run npm build script if package.json is present
if [ -f "package.json" ] if [ -f "package.json" ]
then then
npm install npm install
if [ -f "gulpfile.js" ] npm run build
then
npm run build
fi
fi fi
# Generate the HTML for the documentation # Generate the HTML for the documentation

View File

@ -101,6 +101,7 @@ SUPPORTED_COMBINATIONS = [
{"frontend_pipeline": "None"}, {"frontend_pipeline": "None"},
{"frontend_pipeline": "Django Compressor"}, {"frontend_pipeline": "Django Compressor"},
{"frontend_pipeline": "Gulp"}, {"frontend_pipeline": "Gulp"},
{"frontend_pipeline": "Webpack"},
{"use_celery": "y"}, {"use_celery": "y"},
{"use_celery": "n"}, {"use_celery": "n"},
{"use_mailhog": "y"}, {"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 # Generate the HTML for the documentation
docker-compose -f local.yml run docs make html 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

@ -348,3 +348,7 @@ vendors.js
*.min.js *.min.js
*.min.js.map *.min.js.map
{%- endif %} {%- 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="celeryworker"/>
<option value="celerybeat"/> <option value="celerybeat"/>
{%- endif %} {%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Gulp' %} {%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
<option value="node"/> <option value="node"/>
{%- endif %} {%- endif %}
</list> </list>

View File

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

View File

@ -128,13 +128,14 @@ See detailed [cookiecutter-django Heroku documentation](http://cookiecutter-djan
See detailed [cookiecutter-django Docker documentation](http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html). See detailed [cookiecutter-django Docker documentation](http://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html).
{%- endif %} {%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Gulp' %} {%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
### Custom Bootstrap Compilation ### Custom Bootstrap Compilation
The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice. The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice.
Bootstrap v5 is installed using npm and customised by tweaking your variables in `static/sass/custom_bootstrap_vars`. Bootstrap v5 is installed using npm and customised by tweaking your variables in `static/sass/custom_bootstrap_vars`.
You can find a list of available variables [in the bootstrap source](https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss), or get explanations on them in the [Bootstrap docs](https://getbootstrap.com/docs/5.1/customize/sass/). You can find a list of available variables [in the bootstrap source](https://github.com/twbs/bootstrap/blob/v5.1.3/scss/_variables.scss), or get explanations on them in the [Bootstrap docs](https://getbootstrap.com/docs/5.1/customize/sass/).
Bootstrap's javascript as well as its dependencies is concatenated into a single file: `static/js/vendors.js`. Bootstrap's javascript as well as its dependencies are concatenated into a single file: `static/js/vendors.js`.
{%- endif %} {%- endif %}

View File

@ -1,6 +1,6 @@
ARG PYTHON_VERSION=3.10-slim-bullseye ARG PYTHON_VERSION=3.10-slim-bullseye
{% if cookiecutter.frontend_pipeline == 'Gulp' -%} {% if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] -%}
FROM node:16-bullseye-slim as client-builder FROM node:16-bullseye-slim as client-builder
ARG APP_HOME=/app ARG APP_HOME=/app
@ -9,6 +9,20 @@ WORKDIR ${APP_HOME}
COPY ./package.json ${APP_HOME} COPY ./package.json ${APP_HOME}
RUN npm install && npm cache clean --force RUN npm install && npm cache clean --force
COPY . ${APP_HOME} COPY . ${APP_HOME}
{%- if cookiecutter.frontend_pipeline == 'Webpack' and cookiecutter.use_whitenoise == 'n' %}
{%- if cookiecutter.cloud_provider == 'AWS' %}
ARG DJANGO_AWS_STORAGE_BUCKET_NAME
ENV DJANGO_AWS_STORAGE_BUCKET_NAME=${DJANGO_AWS_STORAGE_BUCKET_NAME}
ARG DJANGO_AWS_S3_CUSTOM_DOMAIN
ENV DJANGO_AWS_S3_CUSTOM_DOMAIN=${DJANGO_AWS_S3_CUSTOM_DOMAIN}
{%- elif cookiecutter.cloud_provider == 'GCP' %}
ARG DJANGO_GCP_STORAGE_BUCKET_NAME
ENV DJANGO_GCP_STORAGE_BUCKET_NAME=${DJANGO_GCP_STORAGE_BUCKET_NAME}
{%- elif cookiecutter.cloud_provider == 'Azure' %}
ARG DJANGO_AZURE_ACCOUNT_NAME
ENV DJANGO_AZURE_ACCOUNT_NAME=${DJANGO_AZURE_ACCOUNT_NAME}
{%- endif %}
{%- endif %}
RUN npm run build RUN npm run build
{%- endif %} {%- endif %}
@ -99,7 +113,7 @@ RUN chmod +x /start-flower
# copy application code to WORKDIR # 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} COPY --from=client-builder --chown=django:django ${APP_HOME} ${APP_HOME}
{% else %} {% else %}
COPY --chown=django:django . ${APP_HOME} COPY --chown=django:django . ${APP_HOME}

View File

@ -87,6 +87,9 @@ THIRD_PARTY_APPS = [
"corsheaders", "corsheaders",
"drf_spectacular", "drf_spectacular",
{%- endif %} {%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
"webpack_loader",
{%- endif %}
] ]
LOCAL_APPS = [ LOCAL_APPS = [
@ -355,6 +358,19 @@ SPECTACULAR_SETTINGS = {
"VERSION": "1.0.0", "VERSION": "1.0.0",
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"], "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"],
} }
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
# django-webpack-loader
# ------------------------------------------------------------------------------
WEBPACK_LOADER = {
"DEFAULT": {
"CACHE": not DEBUG,
"STATS_FILE": BASE_DIR / "webpack-stats.json",
"POLL_INTERVAL": 0.1,
"IGNORE": [r".+\.hot-update.js", r".+\.map"],
}
}
{%- endif %} {%- endif %}
# Your stuff... # Your stuff...
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

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

View File

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

View File

@ -105,7 +105,7 @@ services:
command: /start-flower command: /start-flower
{%- endif %} {%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Gulp' %} {%- if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] %}
node: node:
build: build:
@ -122,7 +122,9 @@ services:
command: npm run dev command: npm run dev
ports: ports:
- "3000:3000" - "3000:3000"
{%- if cookiecutter.frontend_pipeline == 'Gulp' %}
# Expose browsersync UI: https://www.browsersync.io/docs/options/#option-ui # Expose browsersync UI: https://www.browsersync.io/docs/options/#option-ui
- "3001:3001" - "3001:3001"
{%- endif %}
{%- endif %} {%- endif %}

View File

@ -1,13 +1,17 @@
{ {
"name": "{{cookiecutter.project_slug}}", "name": "{{cookiecutter.project_slug}}",
"version": "{{ cookiecutter.version }}", "version": "{{ cookiecutter.version }}",
"dependencies": {},
"devDependencies": { "devDependencies": {
"bootstrap": "^5.1.3", "@babel/core": "^7.16.5",
"gulp-concat": "^2.6.1", "@babel/preset-env": "^7.16.5",
"@popperjs/core": "^2.10.2", "@popperjs/core": "^2.10.2",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"babel-loader": "^8.2.3",
"bootstrap": "^5.1.3",
"browser-sync": "^2.27.7", "browser-sync": "^2.27.7",
"css-loader": "^6.5.1",
"gulp-concat": "^2.6.1",
"concurrently": "^7.0.0",
"cssnano": "^5.0.11", "cssnano": "^5.0.11",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
@ -16,9 +20,19 @@
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-sass": "^5.0.0", "gulp-sass": "^5.0.0",
"gulp-uglify-es": "^3.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", "pixrem": "^5.0.0",
"postcss": "^8.3.11", "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": { "engines": {
"node": "16" "node": "16"
@ -26,8 +40,11 @@
"browserslist": [ "browserslist": [
"last 2 versions" "last 2 versions"
], ],
"babel": {
"presets": ["@babel/preset-env"]
},
"scripts": { "scripts": {
"dev": "gulp", "dev": "",
"build": "gulp generate-assets" "build": ""
} }
} }

View File

@ -10,6 +10,19 @@ services:
build: build:
context: . context: .
dockerfile: ./compose/production/django/Dockerfile dockerfile: ./compose/production/django/Dockerfile
{%- if cookiecutter.frontend_pipeline == 'Webpack' and cookiecutter.use_whitenoise == 'n' %}
args:
# These variable can be defined in an .env file in the root of the repo
{%- if cookiecutter.cloud_provider == 'AWS' %}
DJANGO_AWS_STORAGE_BUCKET_NAME: ${DJANGO_AWS_STORAGE_BUCKET_NAME}
DJANGO_AWS_S3_CUSTOM_DOMAIN: ${DJANGO_AWS_S3_CUSTOM_DOMAIN}
{%- elif cookiecutter.cloud_provider == 'GCP' %}
DJANGO_GCP_STORAGE_BUCKET_NAME: ${DJANGO_GCP_STORAGE_BUCKET_NAME}
{%- elif cookiecutter.cloud_provider == 'Azure' %}
DJANGO_AZURE_ACCOUNT_NAME: ${DJANGO_AZURE_ACCOUNT_NAME}
{%- endif %}
{%- endif %}
image: {{ cookiecutter.project_slug }}_production_django image: {{ cookiecutter.project_slug }}_production_django
depends_on: depends_on:
- postgres - postgres

View File

@ -42,7 +42,10 @@ django-redis==5.2.0 # https://github.com/jazzband/django-redis
{%- if cookiecutter.use_drf == 'y' %} {%- if cookiecutter.use_drf == 'y' %}
# Django REST Framework # Django REST Framework
djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework
django-cors-headers==3.13.0 # https://github.com/adamchainz/django-cors-headers django-cors-headers==3.13.0 # https://github.com/adamchainz/django-cors-headers
# DRF-spectacular for api documentation # DRF-spectacular for api documentation
drf-spectacular==0.25.1 # https://github.com/tfranzel/drf-spectacular drf-spectacular==0.25.1 # https://github.com/tfranzel/drf-spectacular
{%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
django-webpack-loader==1.8.0 # https://github.com/django-webpack/django-webpack-loader
{%- endif %} {%- 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,20 @@
const { merge } = require('webpack-merge');
const commonConfig = require('./common.config');
module.exports = merge(commonConfig, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
port: 3000,
proxy: {
{%- if cookiecutter.use_docker == 'n' %}
'/': 'http://0.0.0.0:8000',
{%- else %}
'/': 'http://django:8000',
{%- endif %}
},
// We need hot=false (Disable HMR) to set liveReload=true
hot: false,
liveReload: true,
},
});

View File

@ -0,0 +1,28 @@
const { merge } = require('webpack-merge');
const commonConfig = require('./common.config');
// This variable should mirror the one from config/settings/production.py
{%- if cookiecutter.use_whitenoise == 'n' %}
{%- if cookiecutter.cloud_provider == 'AWS' %}
const s3BucketName = process.env.DJANGO_AWS_STORAGE_BUCKET_NAME;
const awsS3Domain = process.env.DJANGO_AWS_S3_CUSTOM_DOMAIN ?
process.env.DJANGO_AWS_S3_CUSTOM_DOMAIN
: `${s3BucketName}.s3.amazonaws.com`;
const staticUrl = `https://${awsS3Domain}/static/`;
{%- elif cookiecutter.cloud_provider == 'GCP' %}
const staticUrl = `https://storage.googleapis.com/${process.env.DJANGO_GCP_STORAGE_BUCKET_NAME}/static/`;
{%- elif cookiecutter.cloud_provider == 'Azure' %}
const staticUrl = `https://${process.env.DJANGO_AZURE_ACCOUNT_NAME}.blob.core.windows.net/static/`;
{%- endif %}
{%- else %}
const staticUrl = '/static/';
{%- endif %}
module.exports = merge(commonConfig, {
mode: 'production',
devtool: 'source-map',
bail: true,
output: {
publicPath: `${staticUrl}webpack_bundles/`,
},
});

View File

@ -1 +1,5 @@
{%- if cookiecutter.frontend_pipeline == 'Webpack' %}
import '../sass/project.scss';
{%- endif %}
/* Project specific Javascript goes here. */ /* 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 "custom_bootstrap_vars";
@import "bootstrap"; @import "~bootstrap/scss/bootstrap";
// project specific CSS goes here // 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 %} {% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}"> <html lang="{{ LANGUAGE_CODE }}">
<head> <head>
@ -13,7 +17,7 @@
{% block css %} {% block css %}
{%- endraw %} {%- endraw %}
{%- if cookiecutter.frontend_pipeline != 'Gulp' %} {%- if cookiecutter.frontend_pipeline in ['None', 'Django Compressor'] %}
{%- raw %} {%- raw %}
<!-- Latest compiled and minified Bootstrap CSS --> <!-- Latest compiled and minified Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
@ -31,6 +35,8 @@
{% endcompress %} {% endcompress %}
{%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %} {%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %}
<link href="{% static 'css/project.min.css' %}" rel="stylesheet"> <link href="{% static 'css/project.min.css' %}" rel="stylesheet">
{%- endraw %}{% elif cookiecutter.frontend_pipeline == "Webpack" %}{% raw %}
{% render_bundle 'project' 'css' %}
{%- endraw %}{% endif %}{% raw %} {%- endraw %}{% endif %}{% raw %}
{% endblock %} {% endblock %}
<!-- Le javascript <!-- Le javascript
@ -38,8 +44,11 @@
{# Placed at the top of the document so pages load faster with defer #} {# Placed at the top of the document so pages load faster with defer #}
{% block javascript %} {% block javascript %}
{%- endraw %}{% if cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %} {%- 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> <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 %} {%- endraw %}{% else %}{% raw %}
<!-- Bootstrap JS --> <!-- 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> <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 %} {% endcompress %}
{%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %} {%- endraw %}{% elif cookiecutter.frontend_pipeline == 'Gulp' %}{% raw %}
<script defer src="{% static 'js/project.min.js' %}"></script> <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 %} {%- endraw %}{% endif %}{% raw %}
{% endblock javascript %} {% endblock javascript %}