├── .bumpversion.cfg ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── CHANGELOG.md ├── deployment.md ├── esbuild.md ├── frontend.md ├── images │ ├── react-example.jpg │ ├── readme-head.png │ ├── readme-head.svg │ ├── tailwind-example.png │ └── vue-example.jpg ├── index.md ├── linting.md ├── live_reload.md ├── react.md ├── requirements.txt ├── run_npm_command_at_root.md ├── setup_with_bootstrap.md ├── setup_with_django.md ├── setup_with_flask.md ├── setup_with_tailwind.md ├── setup_with_tailwind3.md ├── swc.md ├── typescript.md └── vue.md ├── material └── overrides │ └── main.html ├── mkdocs.yml ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── setup.cfg └── webpack_boilerplate ├── __init__.py ├── apps.py ├── config.py ├── contrib ├── __init__.py └── jinja2ext.py ├── exceptions.py ├── frontend_template ├── cookiecutter.json ├── hooks │ └── post_gen_project.py └── {{cookiecutter.project_slug}} │ ├── .babelrc │ ├── .browserslistrc │ ├── .eslintrc │ ├── .gitignore │ ├── .nvmrc │ ├── .stylelintrc.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── application │ │ ├── README.md │ │ └── app.js │ ├── components │ │ ├── README.md │ │ └── jumbotron.js │ └── styles │ │ ├── index.css │ │ └── index.scss │ ├── vendors │ ├── .gitkeep │ └── images │ │ ├── .gitkeep │ │ ├── sample.jpg │ │ └── webpack.png │ └── webpack │ ├── webpack.common.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── webpack.config.watch.js ├── loader.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ └── webpack_init.py ├── templatetags ├── __init__.py └── webpack_loader.py └── utils.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.0.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | 8 | [bumpversion:file:webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/package.json] 9 | search = "version": "{current_version}" 10 | replace = "version": "{new_version}" 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: python-webpack-boilerplate 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.10'] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | pip install -r requirements-dev.txt 21 | - name: Check code style 22 | run: | 23 | pre-commit run --all-files 24 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.10' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install poetry 32 | - name: Build package 33 | run: poetry build 34 | - name: Publish package 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | # auto generated 4 | README.rst 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | # Editors and IDEs 65 | .ropeproject/ 66 | 67 | .python-version 68 | 69 | # PyCharm IDE 70 | .idea 71 | .vscode 72 | 73 | .DS_Store 74 | db.sqlite3 75 | node_modules 76 | 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|node_modules|examples|.git|.tox' 2 | default_stages: [commit] 3 | fail_fast: true 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.1.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | 13 | - repo: https://github.com/psf/black 14 | rev: 19.10b0 15 | hooks: 16 | - id: black 17 | additional_dependencies: ['click==8.0.4'] 18 | 19 | - repo: https://github.com/PyCQA/isort 20 | rev: 4.3.21 21 | hooks: 22 | - id: isort 23 | 24 | - repo: https://github.com/PyCQA/flake8 25 | rev: 3.7.9 26 | hooks: 27 | - id: flake8 28 | args: ['--config=setup.cfg'] 29 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.8" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v1.0.4 4 | 5 | 1. Upgrade nvmrc to use Node v22. 6 | 2. Upgrade the frontend dependency 7 | 3. Add `Tailwind`, `Bootstrap` and the `SCSS` as the pre-built style solutions. 8 | 4. Add doc for `Tailwind v4` 9 | 10 | ## v1.0.3 11 | 12 | 1. Remove `Bootstrap` from the default setup. 13 | 2. Update doc about `Bootstrap`, `SWC`, `React`, `Vue` 14 | 3. Update frontend dependency to the latest version 15 | 16 | ## v1.0.2 17 | 18 | 1. Support Node LTS Iron 19 | 2. Upgrade some frontend dependency 20 | 3. Switch from `@babel/plugin-proposal-class-properties` to `@babel/plugin-transform-class-properties` 21 | 22 | ## v1.0.1 23 | 24 | 1. Upgrade some frontend dependency 25 | 26 | ## v1.0.0 27 | 28 | 1. Upgrade nearly all frontend dependency package. 29 | 1. Drop `file-loader`, use `Webpack Asset` instead 30 | 1. Remove deprecated package `babel/polyfil` 31 | 1. Update Tailwind doc for v3 32 | 33 | ## v0.0.10 34 | 35 | 1. Fix `stylelint` when `run_npm_command_at_root` 36 | 37 | ## v0.0.9 38 | 39 | 1. Fix missing `postcss.config.js` when `run_npm_command_at_root` 40 | 41 | ## v0.0.8 42 | 43 | 1. Drop IE support in `browserslist` 44 | 1. Add `run_npm_command_at_root`, so user can run `npm` command under the root directory instead of the `frontend` directory. 45 | 1. Add `esbuild` support in doc 46 | 1. Add `live reload` feature in doc 47 | 48 | ## v0.0.7 49 | 50 | 1. Makes `webpack_loader` render Django hashed static file 51 | 1. Update `pre-commit-config.yaml` 52 | 53 | ## v0.0.6 54 | 55 | DELETED 56 | 57 | ## v0.0.5 58 | 59 | 1. Use `Poetry` to manage project meta info and dependency 60 | 1. Rename app from `webpack_loader` to `webpack_boilerplate` 61 | 1. Move the `frontend_template` under `webpack_boilerplate` directory 62 | 1. When publishing, upload `frontend_template` so the frontend template is also pinned. 63 | 1. Make `cookiecutter` use `frontend_template` in `webpack_boilerplate` directory instead of the Git repo. 64 | 65 | ## v0.0.4 66 | 67 | 1. Upgrade nvmrc to use `lts/fermium` 68 | 1. Upgrade dependency 69 | 1. Import `postcss-preset-env` 70 | 1. Add doc for `Tailwind CSS` 71 | 72 | ## v0.0.3 73 | 74 | 1. Update README 75 | 1. Better support JS debug 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Accord Box 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | 3 | docs: 4 | mkdocs serve --dev-addr '127.0.0.1:9090' 5 | 6 | build: 7 | poetry build 8 | 9 | publish: 10 | poetry publish 11 | 12 | # poetry config repositories.testpypi https://test.pypi.org/legacy/ 13 | publish-test: 14 | poetry publish -r testpypi 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jump start frontend project bundled by Webpack 2 | 3 | [![Build Status](https://github.com/AccordBox/python-webpack-boilerplate/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/AccordBox/python-webpack-boilerplate/actions/workflows/ci.yml) 4 | [![PyPI version](https://badge.fury.io/py/python-webpack-boilerplate.svg)](https://badge.fury.io/py/python-webpack-boilerplate) 5 | [![Documentation](https://img.shields.io/badge/Documentation-link-green.svg)](https://python-webpack-boilerplate.rtfd.io/) 6 | 7 | Bring modern frontend tooling to Django, Flask, within minutes. 8 | 9 | Key Features: 10 | 11 | 1. Out-of-the-box support for `Tailwind CSS`, `Bootstrap`, and `SCSS`. 12 | 1. Seamless integration of modern JavaScript tooling. 13 | 1. Effortless code linting for JavaScript, CSS, and SCSS. 14 | 1. Comprehensive documentation with detailed configuration instructions. 15 | 16 | ## Documentation 17 | 18 | [Documentation](https://python-webpack-boilerplate.rtfd.io/) 19 | 20 | ## Raising funds 21 | 22 | If you like this project, please consider supporting my work. [Open Collective](https://opencollective.com/python-webpack-boilerplate) 23 | 24 | --- 25 | 26 | 27 | 28 | --- 29 | 30 | ## Special Thanks 31 | 32 | * [cssbundling-rails](https://github.com/rails/cssbundling-rails) 33 | * [jsbundling-rails](https://github.com/rails/jsbundling-rails) 34 | * [shakapacker](https://github.com/shakacode/shakapacker) 35 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment Notes 2 | 3 | ## Solution 1 4 | 5 | On some platform such as Heroku, they have `buildpack` which can detect the project and deploy it automatically. 6 | 7 | 1. Please use `run_npm_command_at_root` to make sure the `package.json` exists at the root directory. [Run NPM command at root directory](run_npm_command_at_root.md) 8 | 1. Enable the `Javascript buildpack` on Heroku, so Heroku will detect `package.json` and run `npm run build` during the deployment. [Using Multiple Buildpacks for an App](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) 9 | 1. If you want to specify the version of `node`, `npm`, please check [https://devcenter.heroku.com/articles/nodejs-support#supported-runtimes](https://devcenter.heroku.com/articles/nodejs-support#supported-runtimes) 10 | 11 | ## Solution 2 12 | 13 | Another solution is Docker, we can write code in `Dockerfile` to deploy. 14 | 15 | One example file is on [https://github.com/AccordBox/django-heroku-docker/blob/master/Dockerfile](https://github.com/AccordBox/django-heroku-docker/blob/master/Dockerfile) 16 | -------------------------------------------------------------------------------- /docs/esbuild.md: -------------------------------------------------------------------------------- 1 | # esbuild 2 | 3 | By default, this project use `Babel` to transpile JS code. 4 | 5 | For better performance, we can switch to `esbuild` 6 | 7 | ## Something you should know 8 | 9 | From `ESbuld` doc [https://esbuild.github.io/content-types/#es5](https://esbuild.github.io/content-types/#es5) 10 | 11 | > Transforming ES6+ syntax to ES5 is not supported yet. However, if you're using esbuild to transform ES5 code, you should still set the target to es5. This prevents esbuild from introducing ES6 syntax into your ES5 code. 12 | 13 | So if your project still need to support some old browser, `Babel` is still better option for you. 14 | 15 | ## Install 16 | 17 | ```bash 18 | $ cd frontend 19 | $ npm install -D esbuild-loader 20 | ``` 21 | 22 | Next, let's change Webpack config to use `esbuild-loader` to process our JS files 23 | 24 | Edit *webpack.config.dev.js* and *webpack.config.watch.js* 25 | 26 | ```js 27 | { 28 | test: /\.js$/, 29 | include: Path.resolve(__dirname, "../src"), 30 | loader: "esbuild-loader", // replace loader for the js files 31 | options: { // we can pass options as we like 32 | target: ['es2015'] 33 | } 34 | }, 35 | ``` 36 | 37 | Edit *webpack.config.prod.js* 38 | 39 | ```js 40 | { 41 | test: /\.js$/, 42 | include: Path.resolve(__dirname, "../src"), 43 | loader: "esbuild-loader", 44 | options: { // we can pass options as we like 45 | target: ['es2015'] 46 | } 47 | }, 48 | ``` 49 | 50 | That is it, now you can run `npm run start` or `npm run build` to compile frontend assets. 51 | 52 | ## Reference 53 | 54 | 1. [https://github.com/privatenumber/esbuild-loader](https://github.com/privatenumber/esbuild-loader) 55 | 1. [https://esbuild.github.io/](https://esbuild.github.io/) 56 | -------------------------------------------------------------------------------- /docs/frontend.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | ## Why Webpack 4 | 5 | !!! note 6 | webpack is an open-source JavaScript module bundler. It is made primarily for JavaScript, but it can transform front-end assets such as HTML, CSS, and images if the corresponding loaders are included 7 | 8 | 1. `Webpack` is the most popular bundle solution in the frontend community today, it has received 50k stars on Github. 9 | 1. It has a great ecosystem, many plugins, loaders. If we search `webpack` on npmjs.com, we can get 20k resulst. 10 | 1. If we do not need `React`, `Vue`, we can still use Webpack to help us compile `ES6`, `SCSS` and do many other things (Many people do not know that!) 11 | 1. With a proper config, Webpack can save time and let us build modern web application in quick way. 12 | 13 | ## Structures 14 | 15 | After creating frontend project from the template, you will have file structures like this. 16 | 17 | ``` 18 | frontend 19 | ├── package.json 20 | ├── src 21 | │   ├── application 22 | │   ├── components 23 | │   └── styles 24 | ├── vendors 25 | │   └── images 26 | │   ├── sample.jpg 27 | │   └── webpack.png 28 | ``` 29 | 30 | 1. Components can be placed at `src/components` 31 | 1. SCSS and CSS code can be put at `src/styles` 32 | 1. Static assets such as images, fonts and other files can be put at `vendors` 33 | 34 | ## Compile 35 | 36 | !!! note 37 | If you have no nodejs installed, please install it first by using below links 38 | 39 | 1. On [nodejs homepage](https://nodejs.org/en/download/) 40 | 1. Using [nvm](https://github.com/nvm-sh/nvm) I recommend this way. 41 | 42 | ```bash 43 | # install dependency packages 44 | $ npm install 45 | # run webpack in watch mode 46 | $ npm run watch 47 | ``` 48 | 49 | You will see `build` directory is created under `frontend` directory 50 | 51 | 1. `manifest.json` contains assets manifest info. 52 | 1. We can get the dependency of the entrypoint through `manifest.json` 53 | 1. So in templates, we can only import entrypoint without dependency files. 54 | 55 | For example, `{{ javascript_pack('app', attrs='charset="UTF-8"') }}` would generate HTMl like this 56 | 57 | ```html 58 | 59 | 60 | 61 | ``` 62 | 63 | 1. `app` is entrypoint file 64 | 1. `/static/js/runtime.js` and `/static/js/vendors-node_modules_bootstrap_dist_js_bootstrap_bundle_js.js` are all dependency files. 65 | 1. `javascript_pack` helps us import bundle files transparently 66 | 67 | ## NPM commands 68 | 69 | There are three npm commands for us to use 70 | 71 | ### npm run watch 72 | 73 | `npm run watch` would run webpack in `watch` mode. 74 | 75 | webpack can watch files and recompile whenever they change 76 | 77 | ### npm run start 78 | 79 | `npm run start` will launch a server process, which makes `live reloading` possible. 80 | 81 | If you change JS or SCSS files, the web page would auto refresh after the change. Now the server is working on port 9091 by default, but you can change it in `webpack/webpack.config.dev.js` 82 | 83 | ### npm run build 84 | 85 | [production mode](https://webpack.js.org/guides/production/), Webpack would focus on minified bundles, lighter weight source maps, and optimized assets to improve load time. 86 | 87 | ### What should I use 88 | 89 | 1. If you are developing, use `npm run watch` or `npm run start` 90 | 1. You should use `npm run build` in deployment 91 | 92 | ## Deployment Notes 93 | 94 | [Deployment Notes](deployment.md) 95 | -------------------------------------------------------------------------------- /docs/images/react-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/docs/images/react-example.jpg -------------------------------------------------------------------------------- /docs/images/readme-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/docs/images/readme-head.png -------------------------------------------------------------------------------- /docs/images/readme-head.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Python Webpack Boilerplate 10 | 11 | 12 | Jump start frontend project bundled by Webpack 13 | 14 | 15 | Support: (✓)Django (✓)Flask 16 | 17 | -------------------------------------------------------------------------------- /docs/images/tailwind-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/docs/images/tailwind-example.png -------------------------------------------------------------------------------- /docs/images/vue-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/docs/images/vue-example.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ![](./images/readme-head.svg) 4 | 5 | ## Setup 6 | 7 | 1. [Setup with Django](setup_with_django.md) 8 | 1. [Setup with Flask](setup_with_flask.md) 9 | 10 | ## Style Solutions 11 | 12 | 1. [Tailwind CSS v4](setup_with_tailwind.md) 13 | 2. [Tailwind CSS v3](setup_with_tailwind3.md) 14 | 3. [Bootstrap](setup_with_bootstrap.md) 15 | 16 | ## Features 17 | 18 | 1. [Frontend Workflow](frontend.md) 19 | 2. [Live Reload](live_reload.md) 20 | 3. [Typescript](typescript.md) 21 | 22 | ## Advanced 23 | 24 | 1. [React](react.md) 25 | 2. [Vue](vue.md) 26 | 27 | ## Speed Up 28 | 29 | 1. [ESBuild](esbuild.md) 30 | 2. [SWC](swc.md) 31 | 32 | ## Deployment 33 | 34 | 1. [Deployment Notes](deployment.md) 35 | -------------------------------------------------------------------------------- /docs/linting.md: -------------------------------------------------------------------------------- 1 | # Code Linting 2 | 3 | Code linting on the frontend can guarantee the code quality and make the code more readable. 4 | 5 | We use `eslint` and `stylelint` to lint the code, and you can just use them out of the box. 6 | 7 | ## ESLint 8 | 9 | ESLint is a static code analysis tool for identifying problematic patterns found in JavaScript code. It helps maintain code quality by enforcing coding standards and best practices. Some benefits of using ESLint include: 10 | 11 | - **Code Consistency:** ESLint ensures that all code in the project follows the same style and guidelines, making it easier for developers to collaborate. 12 | - **Error Prevention:** It helps catch common programming errors and potential bugs during development. 13 | - **Customizable Rules:** ESLint allows you to configure rules based on your project's specific requirements. 14 | - **Integration:** ESLint can be easily integrated into various build tools and IDEs. 15 | 16 | ## Stylelint 17 | 18 | Stylelint is a linting tool for CSS and SCSS that helps maintain consistent coding styles and prevents errors in your stylesheets. Some benefits of using Stylelint include: 19 | 20 | - **Consistent Styles:** Stylelint enforces consistent coding styles across your stylesheets, ensuring a uniform look and feel. 21 | - **Error Detection:** It helps identify errors and potential issues in your CSS code. 22 | - **Customizable Configuration:** Stylelint allows you to configure rules and plugins to suit your project's needs. 23 | - **Integration:** Stylelint can be seamlessly integrated into your build process and editor. -------------------------------------------------------------------------------- /docs/live_reload.md: -------------------------------------------------------------------------------- 1 | # Live Reload 2 | 3 | With `webpack-dev-server`, we can use it to auto reload web page when the code of the project changed. 4 | 5 | Please edit `frontend/webpack/webpack.config.dev.js` 6 | 7 | ```js 8 | devServer: { 9 | host: "0.0.0.0", 10 | port: 9091, 11 | headers: { 12 | "Access-Control-Allow-Origin": "*", 13 | }, 14 | devMiddleware: { 15 | writeToDisk: true, 16 | }, 17 | watchFiles: [ 18 | Path.join(__dirname, '../../django_app/**/*.py'), 19 | Path.join(__dirname, '../../django_app/**/*.html'), 20 | ], 21 | }, 22 | ``` 23 | 24 | Notes: 25 | 26 | 1. Add `watchFiles` to the `devServer` object. 27 | 1. Here we tell `webpack-dev-server` to watch all `.py` and `.html` files under the `django_app` directory. 28 | 29 | Run Webpack dev server with `npm run start` 30 | 31 | Now if we change code in the editor, the web page will live reload, which is awesome! 32 | -------------------------------------------------------------------------------- /docs/react.md: -------------------------------------------------------------------------------- 1 | # Setup React 2 | 3 | ## Install Dependency 4 | 5 | Now go to directory which contains `package.json`, by default, it is root directory. 6 | 7 | ```bash 8 | $ npm install @babel/preset-react eslint-plugin-react --save-dev 9 | ``` 10 | 11 | Once done, edit `.babelrc` to make babel to use the above `@babel/preset-react` 12 | 13 | ```json hl_lines="11" 14 | { 15 | ... 16 | "presets": [ 17 | [ 18 | "@babel/preset-env", 19 | { 20 | "useBuiltIns": "usage", 21 | "corejs": "3.0.0" 22 | } 23 | ], 24 | "@babel/preset-react" 25 | ], 26 | ... 27 | } 28 | ``` 29 | 30 | Edit `.eslintrc` to add `plugin:react/recommended` to the `extends`, so `eslint` can recognize React syntax. 31 | 32 | ```js hl_lines="5" 33 | { 34 | ... 35 | "extends": [ 36 | "eslint:recommended", 37 | "plugin:react/recommended" 38 | ], 39 | ... 40 | } 41 | ``` 42 | 43 | Next, let's install React 44 | 45 | ```bash 46 | $ npm install react react-dom 47 | ``` 48 | 49 | That is it, now the frontend project can work with React. 50 | 51 | ## Sample App 52 | 53 | Create `frontend/src/application/app_react.js` 54 | 55 | ```js 56 | import React, { useState, useEffect } from 'react'; 57 | import { createRoot } from 'react-dom/client'; 58 | 59 | 60 | const Clock = () => { 61 | const [time, setTime] = useState(new Date()); 62 | 63 | useEffect(() => { 64 | const timer = setInterval(() => { 65 | setTime(new Date()); 66 | }, 1000); 67 | 68 | // Clear the interval when the component unmounts 69 | return () => clearInterval(timer); 70 | }, []); 71 | 72 | return ( 73 |
74 |

Current Time:

75 |

{time.toLocaleTimeString()}

76 |
77 | ); 78 | }; 79 | 80 | const root = document.getElementById('root'); 81 | const rootElement = createRoot(root); 82 | rootElement.render(); 83 | ``` 84 | 85 | ``` 86 | $ npm run start 87 | ``` 88 | 89 | Edit Django template `templates/index.html` 90 | 91 | ```html hl_lines="25-26" 92 | {% load webpack_loader static %} 93 | 94 | 95 | 96 | 97 | 98 | Index 99 | 100 | {% stylesheet_pack 'app' %} 101 | {% javascript_pack 'app' 'app_react' attrs='defer' %} 102 | 103 | 104 | 105 |
106 |
107 |

Welcome to Our Website

108 |

This is a hero section built using Tailwind CSS.

109 | 110 |
111 | 112 |
113 |
114 |
115 | 116 |
117 |
118 | 119 | 120 | 121 | ``` 122 | 123 | 1. We added a `root` div to let React render the component. 124 | 1. We use `{% javascript_pack 'app' 'app_react' attrs='defer' %}` to load two entrypoint files to the template. 125 | 126 | ```bash 127 | $ python manage.py runserver 128 | ``` 129 | 130 | !!! note 131 | Here we use React to render specific component in the page, and we can still use raw HTML to write other parts, which is convenient 132 | 133 | Here is the screenshot: 134 | 135 | ![React example](images/react-example.jpg) 136 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.1.2 2 | mkdocs-material==6.2.8 3 | jinja2==3.0.3 4 | -------------------------------------------------------------------------------- /docs/run_npm_command_at_root.md: -------------------------------------------------------------------------------- 1 | # Run NPM command at root directory 2 | 3 | !!! note 4 | This feature is turned on by default since `1.0.3` 5 | 6 | By default, the `package.json` will be placed at the `frontend` directory, which means, you need to run `npm` command under the `frontend` directory. 7 | 8 | If you do not like this and want to run `npm` command at the root directory. 9 | 10 | Please set `run_npm_command_at_root` to `y` when you create the `webpack` project. 11 | 12 | Below files will be placed at the root directory instead of the `frontend` directory. 13 | 14 | ``` 15 | .babelrc 16 | .browserslistrc 17 | .eslintrc 18 | .nvmrc 19 | .stylelintrc.json 20 | package-lock.json 21 | package.json 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/setup_with_bootstrap.md: -------------------------------------------------------------------------------- 1 | # Bootstrap 2 | 3 | After you create the frontend project using this project 4 | 5 | ```bash 6 | $ npm install bootstrap 7 | ``` 8 | 9 | Then you will see something like this in the package.json 10 | 11 | ```json 12 | { 13 | "dependencies": { 14 | "bootstrap": "^5.3.0" 15 | } 16 | } 17 | ``` 18 | 19 | Import Bootstrap in `src/application/app.js` 20 | 21 | ```javascript 22 | import "bootstrap/dist/js/bootstrap.bundle"; 23 | ``` 24 | 25 | Import Bootstrap SCSS in `src/styles/index.scss` 26 | 27 | ```scss 28 | // If you comment out code below, bootstrap will use red as primary color 29 | // and btn-primary will become red 30 | 31 | // $primary: red; 32 | 33 | @import "~bootstrap/scss/bootstrap.scss"; 34 | ``` 35 | 36 | That is it, you can run `npm run watch` and check the result in the browser. 37 | 38 | ## Reference 39 | 40 | [https://getbootstrap.com/docs/5.0/getting-started/webpack/](https://getbootstrap.com/docs/5.0/getting-started/webpack/) 41 | -------------------------------------------------------------------------------- /docs/setup_with_django.md: -------------------------------------------------------------------------------- 1 | # Setup With Django 2 | 3 | We will show you how to integrate `python-webpack-boilerplate` with Django and use Tailwind v4 as the style solution. 4 | 5 | ## Install Package 6 | 7 | ```bash 8 | $ pip install Django 9 | $ django-admin startproject example 10 | $ cd example 11 | ``` 12 | 13 | Now your Django projects would seem like this: 14 | 15 | ``` 16 | ├── manage.py 17 | └── example 18 | ├── __init__.py 19 | ├── asgi.py 20 | ├── settings.py 21 | ├── urls.py 22 | └── wsgi.py 23 | ``` 24 | 25 | Next, install package 26 | 27 | ```bash 28 | $ pip install python-webpack-boilerplate 29 | ``` 30 | 31 | Add `webpack_boilerplate` to the `INSTALLED_APPS` in `example/settings.py` 32 | 33 | ```python hl_lines="9" 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'webpack_boilerplate', 43 | ] 44 | ``` 45 | 46 | Let's run Django command to create frontend project from the templates 47 | 48 | ```bash 49 | $ python manage.py webpack_init 50 | [1/3] project_slug (frontend): 51 | [2/3] run_npm_command_at_root (y): 52 | [3/3] Select style_solution 53 | 1 - tailwind 54 | 2 - bootstrap 55 | 3 - scss 56 | Choose from [1/2/3] (1): 1 57 | [SUCCESS]: Frontend app 'frontend' has been created. To know more, check https://python-webpack-boilerplate.rtfd.io/en/latest/frontend/ 58 | ``` 59 | 60 | Here we use the default `Tailwind CSS` as our style solution. 61 | 62 | Now a new `frontend` directory is created which contains pre-defined files for our frontend project. 63 | 64 | ```bash 65 | ├── frontend 66 | │   ├── src 67 | │   ├── vendors 68 | │   └── webpack 69 | ├── manage.py 70 | ├── package-lock.json 71 | ├── package.json 72 | ├── postcss.config.js 73 | └── requirements.txt 74 | ``` 75 | 76 | ## Frontend 77 | 78 | !!! note 79 | If you have no nodejs installed, please install it first by using below links 80 | 81 | 1. On [nodejs homepage](https://nodejs.org/en/download/) 82 | 1. Using [nvm](https://github.com/nvm-sh/nvm) I recommend this way. 83 | 84 | ```bash 85 | $ node -v 86 | v22.13.1 87 | $ npm -v 88 | 10.9.2 89 | ``` 90 | 91 | Now go to directory which contains `package.json`, by default, it is root directory. 92 | 93 | ```bash 94 | # install dependency packages 95 | $ npm install 96 | 97 | # run webpack in watch mode 98 | $ npm run watch 99 | ``` 100 | 101 | !!! note 102 | run watch means webpack will watch source files and recompile whenever they change 103 | 104 | The build files now can be found in `frontend/build` directory 105 | 106 | ``` 107 | build 108 | ├── css 109 | ├── js 110 | ├── manifest.json 111 | └── vendors 112 | ``` 113 | 114 | !!! note 115 | You can check [Frontend Workflow](frontend.md) to learn more details about the frontend workflow 116 | 117 | After Webpack has compiled the assets, we can load the assets in Django. 118 | 119 | ## Config Django 120 | 121 | Add code below to Django settings `example/settings.py` 122 | 123 | ```python 124 | STATICFILES_DIRS = [ 125 | BASE_DIR / "frontend/build", 126 | ] 127 | 128 | WEBPACK_LOADER = { 129 | 'MANIFEST_FILE': BASE_DIR / "frontend/build/manifest.json", 130 | } 131 | ``` 132 | 133 | 1. We add the above `frontend/build` to `STATICFILES_DIRS` so Django can find the static assets (img, font and others) 134 | 1. We add `MANIFEST_FILE` location to the `WEBPACK_LOADER` so our `webpack_loader` can know where to find the manifest file. 135 | 136 | ## Load the bundle files 137 | 138 | Let's do a quick test on the home page. 139 | 140 | Update `example/urls.py` 141 | 142 | ```python hl_lines="6" 143 | from django.contrib import admin 144 | from django.urls import path 145 | from django.views.generic import TemplateView 146 | 147 | urlpatterns = [ 148 | path('', TemplateView.as_view(template_name="index.html")), # new 149 | path('admin/', admin.site.urls), 150 | ] 151 | ``` 152 | 153 | ```bash hl_lines="9" 154 | $ mkdir example/templates 155 | 156 | ├── frontend 157 | ├── manage.py 158 | └── example 159 | ├── __init__.py 160 | ├── asgi.py 161 | ├── settings.py 162 | ├── templates 163 | ├── urls.py 164 | └── wsgi.py 165 | ``` 166 | 167 | Update `TEMPLATES` in `example/settings.py`, so Django can know where to find the templates 168 | 169 | ```python hl_lines="4" 170 | TEMPLATES = [ 171 | { 172 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 173 | 'DIRS': ['example/templates'], 174 | 'APP_DIRS': True, 175 | 'OPTIONS': { 176 | 'context_processors': [ 177 | 'django.template.context_processors.debug', 178 | 'django.template.context_processors.request', 179 | 'django.contrib.auth.context_processors.auth', 180 | 'django.contrib.messages.context_processors.messages', 181 | ], 182 | }, 183 | }, 184 | ] 185 | ``` 186 | 187 | Add `index.html` to the above `example/templates` 188 | 189 | ```html 190 | {% load webpack_loader static %} 191 | 192 | 193 | 194 | 195 | Index 196 | 197 | 198 | {% stylesheet_pack 'app' %} 199 | 200 | 201 | 202 |
203 |
204 |

Hello, world!

205 |

This is a template for a simple marketing or informational website. It includes a large callout called a 206 | jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.

207 | 208 |

Learn more »

209 | 210 |
211 | 212 |
213 |
214 |
215 | 216 | {% javascript_pack 'app' %} 217 | 218 | 219 | 220 | ``` 221 | 222 | 1. We `load webpack_loader` at the top of the template 223 | 2. We can still use `static` tag to import image from the frontend project. 224 | 3. We use `stylesheet_pack` and `javascript_pack` to load CSS and JS bundle files to Django 225 | 226 | !!! note 227 | 1. When your javascript and css files grow bigger and bigger, code splitting would be done automatically by Webpack. 228 | 1. The `javascript_pack` would **import dependency files automatically to handle code splitting** 229 | 1. You can import **multiple entry files** using `stylesheet_pack` and `javascript_pack` (`{% javascript_pack 'app' 'vendors'`) if you prefer manual code splitting. 230 | 231 | ## Manual Test 232 | 233 | ```bash 234 | # restart webpack to let Tailwind auto scan 235 | $ npm run watch 236 | 237 | $ python manage.py migrate 238 | $ python manage.py runserver 239 | ``` 240 | 241 | Now check on [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and you should be able to see a welcome page. 242 | 243 | In the devtools console, you should see 244 | 245 | ```bash 246 | dom ready 247 | jumbotron.js:8 Jumbotron initialized for node: [object HTMLDivElement] 248 | ``` 249 | 250 | ## Explicitly Specify Source Files 251 | 252 | Even Tailwind 4 can AUTO scan all project files in the project directory, we still recommend to explicitly specify the source files to improve performance. 253 | 254 | Below is an example of `frontend/src/styles/index.css` 255 | 256 | ```css 257 | /*import tailwindcss and disable automatic source detection*/ 258 | @import "tailwindcss" source(none); 259 | 260 | /*register frontend directory*/ 261 | @source "../"; 262 | 263 | /*register django templates*/ 264 | @source "../../../django_tailwind_app/**/*.html"; 265 | 266 | .jumbotron { 267 | /*should be relative path of the entry css file*/ 268 | background-image: url("../../vendors/images/sample.jpg"); 269 | background-size: cover; 270 | } 271 | 272 | @layer components{ 273 | .btn-blue { 274 | @apply inline-flex items-center; 275 | @apply px-4 py-2; 276 | @apply font-semibold rounded-lg shadow-md; 277 | @apply text-white bg-blue-500; 278 | @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/50; 279 | } 280 | } 281 | ``` 282 | 283 | ## Live Reload 284 | 285 | [Live Reload Support](live_reload.md) 286 | -------------------------------------------------------------------------------- /docs/setup_with_flask.md: -------------------------------------------------------------------------------- 1 | # Setup With Flask 2 | 3 | ## Install Package 4 | 5 | ```bash 6 | $ pip install Flask 7 | $ mkdir example 8 | $ cd example 9 | ``` 10 | 11 | Next, install package 12 | 13 | ```bash 14 | $ pip install python-webpack-boilerplate 15 | ``` 16 | 17 | Create `app.py` 18 | 19 | ```python 20 | import os 21 | from flask import Flask 22 | 23 | app = Flask(__name__) 24 | 25 | @app.cli.command("webpack_init") 26 | def webpack_init(): 27 | from cookiecutter.main import cookiecutter 28 | import webpack_boilerplate 29 | pkg_path = os.path.dirname(webpack_boilerplate.__file__) 30 | cookiecutter(pkg_path, directory="frontend_template") 31 | ``` 32 | 33 | Here we created a `webpack_init` custom command to help us create `frontend` project. 34 | 35 | ```bash 36 | $ env FLASK_APP=app.py flask webpack_init 37 | 38 | project_slug [frontend]: 39 | run_npm_command_at_root [y]: 40 | [SUCCESS]: Frontend app 'frontend' has been created. 41 | ``` 42 | 43 | Now a new `frontend` directory is created which contains pre-defined files for our frontend project. 44 | 45 | ```bash 46 | ├── frontend 47 | │   ├── src 48 | │   ├── vendors 49 | │   └── webpack 50 | ├── manage.py 51 | ├── package-lock.json 52 | ├── package.json 53 | ├── postcss.config.js 54 | └── requirements.txt 55 | ``` 56 | 57 | ## Frontend 58 | 59 | !!! note 60 | If you have no nodejs installed, please install it first by using below links 61 | 62 | 1. On [nodejs homepage](https://nodejs.org/en/download/) 63 | 1. Using [nvm](https://github.com/nvm-sh/nvm) I recommend this way. 64 | 65 | ```bash 66 | $ node -v 67 | v20.9.0 68 | $ npm -v 69 | 10.1.0 70 | ``` 71 | 72 | Now go to directory which contains `package.json`, by default, it is root directory. 73 | 74 | ```bash 75 | # install dependency packages 76 | $ npm install 77 | 78 | # run webpack in watch mode 79 | $ npm run watch 80 | ``` 81 | 82 | !!! note 83 | run watch means webpack will watch source files and recompile whenever they change 84 | 85 | The build files now can be found in `frontend/build` directory 86 | 87 | ``` 88 | build 89 | ├── css 90 | ├── js 91 | ├── manifest.json 92 | └── vendors 93 | ``` 94 | 95 | !!! note 96 | You can check [Frontend Workflow](frontend.md) to learn more about frontend stuff 97 | 98 | ## Config Flask 99 | 100 | Update `app.py` 101 | 102 | ```python 103 | import os 104 | from pathlib import Path 105 | from flask import Flask, render_template 106 | from webpack_boilerplate.config import setup_jinja2_ext 107 | 108 | 109 | BASE_DIR = Path(__file__).parent 110 | app = Flask(__name__, static_folder="frontend/build", static_url_path="/static/") 111 | app.config.update({ 112 | 'WEBPACK_LOADER': { 113 | 'MANIFEST_FILE': BASE_DIR / "frontend/build/manifest.json", 114 | } 115 | }) 116 | setup_jinja2_ext(app) 117 | 118 | 119 | @app.cli.command("webpack_init") 120 | def webpack_init(): 121 | from cookiecutter.main import cookiecutter 122 | import webpack_boilerplate 123 | pkg_path = os.path.dirname(webpack_boilerplate.__file__) 124 | cookiecutter(pkg_path, directory="frontend_template") 125 | 126 | 127 | @app.route("/") 128 | def hello(): 129 | return render_template('index.html') 130 | ``` 131 | 132 | 1. We add the above `frontend/build` to `static_folder` so Flask can find the static assets (img, font and others) 133 | 1. `static_url_path` is set to `/static/` 134 | 1. We add `MANIFEST_FILE` location to the `WEBPACK_LOADER` so our custom loader can help us load JS and CSS. 135 | 1. Remember to run `setup_jinja2_ext(app)` so we can us custom template tag in Jinja2 136 | 137 | ## Load the bundle files 138 | 139 | Add `index.html` to `templates` 140 | 141 | ``` 142 | ├── app.py 143 | ├── frontend 144 | └── templates 145 | └── index.html 146 | ``` 147 | 148 | ```html 149 | 150 | 151 | 152 | 153 | Index 154 | 155 | {{ stylesheet_pack('app') }} 156 | {{ javascript_pack('app', attrs='defer') }} 157 | 158 | 159 | 160 |
161 |
162 |

Welcome to Our Website

163 |

This is a hero section built using Tailwind CSS.

164 | 165 |
166 | 167 |
168 |
169 |
170 | 171 | 172 | 173 | ``` 174 | 175 | !!! note 176 | 1. Here we use `Tailwind CDN` to help user to do quick test, please remove it later. 177 | 1. You can import multiple entry files using one `javascript_pack` statement 178 | 1. The `javascript_pack` would also **import dependency files automatically to handle code splitting** 179 | 1. You can use `attrs` to set custom attributes 180 | 181 | ## Manual Test 182 | 183 | ```bash 184 | $ env FLASK_APP=app.py flask run 185 | ``` 186 | 187 | Now check on [http://127.0.0.1:5000/](http://127.0.0.1:5000/) and you should be able to see a welcome page. 188 | 189 | The source code can also be found in the [Examples](https://github.com/AccordBox/python-webpack-boilerplate/tree/master/examples/) 190 | -------------------------------------------------------------------------------- /docs/setup_with_tailwind.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS v4 2 | 3 | From `python-webpack-boilerplate>=1.0.4`, we can choose `tailwind` as the style solution when creating the frontend project. 4 | 5 | ```bash 6 | (venv) python manage.py webpack_init 7 | [1/3] project_slug (frontend): 8 | [2/3] run_npm_command_at_root (y): 9 | [3/3] Select style_solution 10 | 1 - tailwind 11 | 2 - bootstrap 12 | 3 - scss 13 | Choose from [1/2/3] (1): 1 14 | [SUCCESS]: Frontend app 'frontend' has been created. To know more, check https://python-webpack-boilerplate.rtfd.io/en/latest/frontend/ 15 | ``` 16 | 17 | Just choose `tailwind` as the style_solution, the Tailwind V4 will be ready for you. 18 | 19 | ## Install Dependency 20 | 21 | In directory which contains the `package.json` 22 | 23 | ```bash 24 | # install dependency packages 25 | $ npm install 26 | # run webpack in watch mode 27 | $ npm run watch 28 | ``` 29 | 30 | ## Explicitly Specify Source Files 31 | 32 | Even Tailwind 4 can AUTO scan all project files in the project directory, we still recommend to explicitly specify the source files to improve performance and avoid webpack keeps recompiling. 33 | 34 | Below is an example 35 | 36 | ```css 37 | /*import tailwindcss and disable automatic source detection*/ 38 | @import "tailwindcss" source(none); 39 | 40 | /*register frontend directory*/ 41 | @source "../"; 42 | 43 | /*register django templates*/ 44 | @source "../../../django_tailwind_app/**/*.html"; 45 | 46 | .jumbotron { 47 | /*should be relative path of the entry css file*/ 48 | background-image: url("../../vendors/images/sample.jpg"); 49 | background-size: cover; 50 | } 51 | 52 | @layer components{ 53 | .btn-blue { 54 | @apply inline-flex items-center; 55 | @apply px-4 py-2; 56 | @apply font-semibold rounded-lg shadow-md; 57 | @apply text-white bg-blue-500; 58 | @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/50; 59 | } 60 | } 61 | ``` 62 | 63 | !!! note 64 | Please remember to update the file path in your project. 65 | 66 | ## Live Reload 67 | 68 | [Live Reload Support](live_reload.md) 69 | 70 | ## Tailwind V3 71 | 72 | If you still want to install Tailwind V3, please check [Tailwind CSS v3](setup_with_tailwind3.md) 73 | -------------------------------------------------------------------------------- /docs/setup_with_tailwind3.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS v3 2 | 3 | This guide will help you install and config `Tailwind v3` 4 | 5 | !!! note 6 | For `python-webpack-boilerplate>=1.0.4`, should choose style solution `scss` when creating the frontend project, and then you can follow this doc 7 | 8 | ## Install Dependency 9 | 10 | In directory which contains the `package.json`, install `tailwindcss` and `postcss-import` 11 | 12 | ```bash 13 | $ npm install -D tailwindcss@3.4.17 postcss-import 14 | ``` 15 | 16 | Once done, update `postcss.config.js` 17 | 18 | ``` 19 | // https://tailwindcss.com/docs/using-with-preprocessors 20 | 21 | module.exports = { 22 | plugins: [ 23 | require('postcss-import'), 24 | require('tailwindcss/nesting')(require('postcss-nesting')), 25 | require('tailwindcss'), 26 | require('postcss-preset-env')({ 27 | features: { 'nesting-rules': false } 28 | }), 29 | ] 30 | }; 31 | ``` 32 | 33 | Next, generate a config file for your project using the Tailwind CLI utility included when you install the `tailwindcss` npm package 34 | 35 | ```bash 36 | $ npx tailwindcss init 37 | ``` 38 | 39 | Now `tailwind.config.js` is generated 40 | 41 | ```js 42 | module.exports = { 43 | content: [], 44 | theme: { 45 | extend: {}, 46 | }, 47 | plugins: [], 48 | } 49 | ``` 50 | 51 | !!! note 52 | 53 | Please make sure `tailwind.config.js` exists in the same directory as `postcss.config.js` 54 | 55 | ## JIT 56 | 57 | From Tailwind V3, it enabled `JIT` (Just-in-Time) all the time. 58 | 59 | > Tailwind CSS works by scanning all of your HTML, JavaScript components, and any other template files for class names, then generating all of the corresponding CSS for those styles. 60 | 61 | > In order for Tailwind to generate all of the CSS you need, it needs to know about every single file in your project that contains any Tailwind class names. 62 | 63 | So we should config `content` section of the `tailwind.config.js`, then Tailwind will know which css classes are used. 64 | 65 | Let's update *frontend/tailwind.config.js* 66 | 67 | ```js 68 | const Path = require("path"); 69 | const pwd = process.env.PWD; 70 | 71 | // We can add current project paths here 72 | const projectPaths = [ 73 | Path.join(pwd, "../example/templates/**/*.html"), 74 | // add js file paths if you need 75 | ]; 76 | 77 | const contentPaths = [...projectPaths]; 78 | console.log(`tailwindcss will scan ${contentPaths}`); 79 | 80 | module.exports = { 81 | content: contentPaths, 82 | theme: { 83 | extend: {}, 84 | }, 85 | plugins: [], 86 | } 87 | ``` 88 | 89 | Notes: 90 | 91 | 1. Here we add Django templates path to the `projectPaths` 92 | 1. And then we pass the `contentPaths` to the `content` 93 | 1. The final built css file will contain css classes used in the Django templates 94 | 95 | ## Import Tailwind CSS 96 | 97 | Update *src/styles/index.scss* 98 | 99 | ```scss 100 | @import "tailwindcss/base"; 101 | @import "tailwindcss/components"; 102 | @import "tailwindcss/utilities"; 103 | 104 | .jumbotron { 105 | // should be relative path of the entry scss file 106 | background-image: url("../../vendors/images/sample.jpg"); 107 | background-size: cover; 108 | } 109 | 110 | .btn-blue { 111 | @apply inline-block px-4 py-2; 112 | @apply font-semibold rounded-lg shadow-md; 113 | @apply bg-blue-500 text-white; 114 | @apply hover:bg-blue-700 focus:outline-none; 115 | @apply focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75; 116 | } 117 | ``` 118 | 119 | ``` 120 | $ npm run start 121 | 122 | tailwindcss will scan django_basic/example/templates/**/*.html 123 | ``` 124 | 125 | Edit Django template `templates/index.html` 126 | 127 | ```html hl_lines="8 28" 128 | {% load webpack_loader static %} 129 | 130 | 131 | 132 | 133 | 134 | Index 135 | {% stylesheet_pack 'app' %} 136 | 137 | 138 | 139 |
140 |
141 |

Hello, world!

142 | 143 |

This is a template for a simple marketing or informational website. It includes a large callout called a 144 | jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.

145 | 146 |

Learn more »

147 | 148 |
149 | 150 |
151 | 152 |
153 |
154 | 155 | {% javascript_pack 'app' %} 156 | 157 | 158 | 159 | ``` 160 | 161 | ```bash 162 | $ python manage.py runserver 163 | ``` 164 | 165 | ![TailwindCSS example](images/tailwind-example.png) 166 | 167 | ## Live Reload 168 | 169 | When you add Tailwind css class in Django template, it would be cool if the page can `auto live realod`, please check link below 170 | 171 | [Live Reload Support](live_reload.md) 172 | 173 | ## Tutorials 174 | 175 | To learn more about Tailwind and Django, you can check 176 | 177 | 1. [Django Tailwind CSS Alpine.js Tutorial](https://www.accordbox.com/blog/django-tailwind-css-alpinejs-tutorial/) 178 | 2. [wagtail-tailwind-blog](https://github.com/AccordBox/wagtail-tailwind-blog) 179 | -------------------------------------------------------------------------------- /docs/swc.md: -------------------------------------------------------------------------------- 1 | # SWC (Speedy Web Compiler) 2 | 3 | SWC is an extensible Rust-based platform for the next generation of fast developer tools. 4 | 5 | While Babel mainly focuses on compatibility and transpiling JavaScript code, SWC provides a broader set of optimizations and features to improve the overall development experience. 6 | 7 | SWC supports both JavaScript and TypeScript, making it suitable for a wide range of frontend projects. 8 | 9 | Additionally, SWC's focus on performance optimizations sets it apart from Babel, as it can significantly reduce build times and enhance runtime performance. 10 | 11 | After switching to `SWC`, the build time is optimized for about 20-30% based on my experience. 12 | 13 | Notes: 14 | 15 | 1. Just install `swc-loader` and `@swc/core`. 16 | 2. Config Webpack rules to use `swc-loader` to process JS files. 17 | 3. Customize the `.swcrc` file if you need. 18 | 4. Even SWC also supports bundling, we still use Webpack to bundle the frontend code. Because Webpack has more features and is more mature than SWC bundling. 19 | 20 | ## Reference 21 | 22 | 1. [https://blog.logrocket.com/migrating-swc-webpack-babel-overview/](https://blog.logrocket.com/migrating-swc-webpack-babel-overview/) 23 | 2. [We exchanged Babel&TSC for SWC and it was worth it!](https://engineering.telia.no/blog/we-exchanged-babel-tsc-for-swc-and-it-was-worth-it) 24 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | # Typescript support 2 | 3 | ## Option 1 4 | 5 | Please check [How to add Typescript to the Django Project](https://www.accordbox.com/blog/how-to-add-typescript-to-the-django-project/) 6 | 7 | ## Option 2 8 | 9 | Or you can just use SWC to compile Typescript code 10 | 11 | [Use SWC](swc.md) 12 | -------------------------------------------------------------------------------- /docs/vue.md: -------------------------------------------------------------------------------- 1 | # Setup Vue 2 | 3 | ## Install 4 | 5 | !!! note 6 | We will setup Vue 3 in this tutorial 7 | 8 | Now go to directory which contains `package.json`, by default, it is root directory. 9 | 10 | ```bash 11 | $ npm install vue-loader @vue/compiler-sfc --save-dev 12 | # install vue 13 | $ npm install vue@3 14 | ``` 15 | 16 | Edit `frontend/webpack/webpack.common.js` 17 | 18 | ```js 19 | const { VueLoaderPlugin } = require('vue-loader'); 20 | 21 | plugins: [ 22 | ... 23 | 24 | new VueLoaderPlugin() 25 | ], 26 | 27 | module: { 28 | rules: [ 29 | ... 30 | 31 | { 32 | test: /\.vue$/, 33 | loader: "vue-loader", 34 | }, 35 | ], 36 | }, 37 | ``` 38 | 39 | 1. Add `VueLoaderPlugin` to `plugins` 40 | 1. Please also add rule to the `module.rules` to let webpack use `vue-loader` to process `.vue` files. 41 | 42 | That is it, now the frontend project should work with Vue. 43 | 44 | ## Sample App 45 | 46 | Create `frontend/src/components/HelloWorld.vue` 47 | 48 | ```vue 49 | 59 | 60 | 68 | 69 | 70 | 86 | ``` 87 | 88 | Create `frontend/src/components/App.vue` 89 | 90 | ```vue 91 | 96 | 97 | 107 | 108 | 118 | ``` 119 | 120 | In the `App.vue`, we import `HelloWorld.vue` and render it under `
` 121 | 122 | Create `frontend/src/application/app_vue.js` 123 | 124 | ```js 125 | import { createApp } from 'vue'; 126 | import App from '../components/App.vue'; 127 | 128 | createApp(App).mount('#app'); 129 | ``` 130 | 131 | Now the file structures would seem like this: 132 | 133 | ``` 134 | $ npm run start 135 | ``` 136 | 137 | Edit Django template `templates/index.html` 138 | 139 | ```html hl_lines="9 10" 140 | {% load webpack_loader static %} 141 | 142 | 143 | 144 | 145 | 146 | Index 147 | 148 | {% stylesheet_pack 'app' 'app_vue' %} 149 | {% javascript_pack 'app' 'app_vue' attrs='defer' %} 150 | 151 | 152 | 153 |
154 |
155 |

Welcome to Our Website

156 |

This is a hero section built using Tailwind CSS.

157 | 158 |
159 | 160 |
161 |
162 |
163 | 164 |
165 |
166 | 167 | 168 | 169 | ``` 170 | 171 | ```bash 172 | $ python manage.py runserver 173 | ``` 174 | 175 | !!! note 176 | Here we use Vue to render specific component in the page, and we can still use raw HTML to write other parts, which is convenient 177 | 178 | ![Vue example](images/vue-example.jpg) 179 | -------------------------------------------------------------------------------- /material/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block announce %} 5 | Have questions, feedback, or just want to chat? Reach out to me on 6 | 7 | 10 | Twitter 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python Webpack Boilerplate 2 | site_url: https://python-webpack-boilerplate.readthedocs.io/ 3 | repo_url: https://github.com/AccordBox/python-webpack-boilerplate 4 | theme: 5 | name: "material" 6 | custom_dir: material/overrides 7 | palette: 8 | primary: teal 9 | markdown_extensions: 10 | - codehilite 11 | - pymdownx.superfences: 12 | - markdown.extensions.admonition: 13 | - pymdownx.details 14 | plugins: 15 | - search 16 | nav: 17 | - Introduction: index.md 18 | - Setup With Django: setup_with_django.md 19 | - Setup With Flask: setup_with_flask.md 20 | - Frontend workflow: frontend.md 21 | - Code Linting: linting.md 22 | - Live Reload: live_reload.md 23 | - Tailwind CSS v4: setup_with_tailwind.md 24 | - Tailwind CSS v3: setup_with_tailwind3.md 25 | - Bootstrap: setup_with_bootstrap.md 26 | - React: react.md 27 | - Vue: vue.md 28 | - Use SWC: swc.md 29 | - Use ESBuild: esbuild.md 30 | - Typescript: typescript.md 31 | - Deployment Notes: deployment.md 32 | - Run NPM command at root directory: run_npm_command_at_root.md 33 | - Change Log: CHANGELOG.md 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-webpack-boilerplate" 3 | version = "1.0.4" 4 | description = "Jump start frontend project bundled by Webpack" 5 | authors = ["Michael Yin "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/AccordBox/python-webpack-boilerplate" 9 | keywords = ["python", "django", "flask", "webpack"] 10 | classifiers = [ 11 | "License :: OSI Approved :: MIT License", 12 | "Topic :: Software Development :: Libraries", 13 | "Topic :: Utilities", 14 | "Environment :: Web Environment", 15 | "Framework :: Django", 16 | "Framework :: Flask", 17 | ] 18 | packages = [ 19 | { include = "webpack_boilerplate" }, 20 | ] 21 | 22 | [tool.poetry.dependencies] 23 | python = "^3.6" 24 | cookiecutter = ">=1.7.0" 25 | 26 | [build-system] 27 | requires = ["setuptools", "poetry_core>=1.0"] 28 | build-backend = "poetry.core.masonry.api" 29 | 30 | [tool.black] 31 | target-version = ['py37'] 32 | include = '\.pyi?$' 33 | exclude = ''' 34 | ( 35 | /( 36 | \.eggs # exclude a few common directories in the 37 | | \.git # root of the project 38 | | \.hg 39 | | \.mypy_cache 40 | | \.pytest_cache 41 | | \.tox 42 | | \.venv 43 | | fixtures 44 | | static 45 | | staticfiles 46 | | templates 47 | | migrations # also separately exclude django migrations folder in apps 48 | )/ 49 | ) 50 | ''' 51 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.tests_django.dj_settings 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | cookiecutter 2 | 3 | mkdocs==1.1.2 4 | mkdocs-material==6.2.8 5 | jinja2<3.1.0 6 | 7 | pre-commit==2.6.0 8 | poetry==1.1.11 9 | 10 | bumpversion==0.6.0 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | include_trailing_comma = True 3 | multi_line_output = 3 4 | not_skip = __init__.py 5 | line_length = 88 6 | 7 | [flake8] 8 | ignore=E501,C901,F401, W504, W503, E203, E231 9 | exclude = node_modules 10 | max-line-length = 119 11 | -------------------------------------------------------------------------------- /webpack_boilerplate/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import django 3 | except ImportError: 4 | pass 5 | else: 6 | if django.VERSION < (3, 2): 7 | default_app_config = "webpack_boilerplate.apps.WebpackBoilerplateConfig" 8 | -------------------------------------------------------------------------------- /webpack_boilerplate/apps.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.apps import AppConfig 3 | except ImportError: 4 | from types import NoneType 5 | 6 | AppConfig = NoneType 7 | 8 | 9 | class WebpackBoilerplateConfig(AppConfig): 10 | name = "webpack_boilerplate" 11 | verbose_name = "Webpack Boilerplate" 12 | -------------------------------------------------------------------------------- /webpack_boilerplate/config.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | __all__ = ("load_config", "get_setting_value") 4 | 5 | 6 | def load_from_django(): 7 | from django.conf import settings 8 | 9 | DEFAULT_CONFIG = { 10 | "CACHE": not settings.DEBUG, 11 | "IGNORE": [r".+\.hot-update.js", r".+\.map"], 12 | "LOADER_CLASS": "webpack_boilerplate.loader.WebpackLoader", 13 | } 14 | 15 | user_config = dict(DEFAULT_CONFIG, **getattr(settings, "WEBPACK_LOADER", {})) 16 | 17 | user_config["ignores"] = [re.compile(I) for I in user_config["IGNORE"]] 18 | user_config["web_framework"] = "django" 19 | return user_config 20 | 21 | 22 | def load_from_flask(): 23 | from flask import current_app 24 | 25 | DEFAULT_CONFIG = { 26 | "CACHE": not current_app.config["DEBUG"], 27 | "IGNORE": [r".+\.hot-update.js", r".+\.map"], 28 | "LOADER_CLASS": "webpack_boilerplate.loader.WebpackLoader", 29 | } 30 | 31 | user_config = dict(DEFAULT_CONFIG, **current_app.config["WEBPACK_LOADER"]) 32 | 33 | user_config["ignores"] = [re.compile(I) for I in user_config["IGNORE"]] 34 | user_config["web_framework"] = "flask" 35 | return user_config 36 | 37 | 38 | def load_config(name): 39 | try: 40 | import django 41 | 42 | return load_from_django() 43 | except ImportError: 44 | pass 45 | 46 | try: 47 | import flask 48 | 49 | return load_from_flask() 50 | except ImportError: 51 | pass 52 | 53 | raise Exception("can not load config from this project") 54 | 55 | 56 | def get_setting_value(key): 57 | try: 58 | import django 59 | from django.conf import settings 60 | 61 | return settings.get(key, None) 62 | except ImportError: 63 | pass 64 | 65 | try: 66 | import flask 67 | from flask import current_app 68 | 69 | map = {"STATIC_URL": "STATIC_URL"} 70 | return current_app.config.get(map[key], None) 71 | except ImportError: 72 | pass 73 | 74 | 75 | def setup_jinja2_ext(app): 76 | """ 77 | Run by flask app 78 | """ 79 | from .contrib.jinja2ext import WebpackExtension 80 | 81 | app.jinja_env.add_extension(WebpackExtension) 82 | -------------------------------------------------------------------------------- /webpack_boilerplate/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/contrib/__init__.py -------------------------------------------------------------------------------- /webpack_boilerplate/contrib/jinja2ext.py: -------------------------------------------------------------------------------- 1 | import jinja2.ext 2 | from markupsafe import Markup 3 | 4 | from .. import utils 5 | 6 | 7 | def stylesheet_pack(*names, **kwargs): 8 | """ 9 | TODO: refactor 10 | """ 11 | tags = [] 12 | for name in names: 13 | sub_tags = utils.get_as_tags( 14 | name, extension="css", attrs=kwargs.get("attrs", "") 15 | ) 16 | for sub_tag in sub_tags: 17 | if sub_tag not in tags: 18 | tags.append(sub_tag) 19 | return Markup("\n".join(tags)) 20 | 21 | 22 | def javascript_pack(*names, **kwargs): 23 | """ 24 | TODO: refactor 25 | """ 26 | tags = [] 27 | for name in names: 28 | sub_tags = utils.get_as_tags( 29 | name, extension="js", attrs=kwargs.get("attrs", "") 30 | ) 31 | for sub_tag in sub_tags: 32 | if sub_tag not in tags: 33 | tags.append(sub_tag) 34 | return Markup("\n".join(tags)) 35 | 36 | 37 | class WebpackExtension(jinja2.ext.Extension): 38 | def __init__(self, environment): 39 | super(WebpackExtension, self).__init__(environment) 40 | environment.globals["stylesheet_pack"] = stylesheet_pack 41 | environment.globals["javascript_pack"] = javascript_pack 42 | -------------------------------------------------------------------------------- /webpack_boilerplate/exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "WebpackError", 3 | "WebpackLoaderBadStatsError", 4 | "WebpackLoaderTimeoutError", 5 | "WebpackBundleLookupError", 6 | ) 7 | 8 | 9 | class BaseWebpackLoaderException(Exception): 10 | """ 11 | Base exception for django-webpack-loader. 12 | """ 13 | 14 | 15 | class WebpackError(BaseWebpackLoaderException): 16 | """ 17 | General webpack loader error. 18 | """ 19 | 20 | 21 | class WebpackLoaderBadStatsError(BaseWebpackLoaderException): 22 | """ 23 | The stats file does not contain valid data. 24 | """ 25 | 26 | 27 | class WebpackLoaderTimeoutError(BaseWebpackLoaderException): 28 | """ 29 | The bundle took too long to compile. 30 | """ 31 | 32 | 33 | class WebpackBundleLookupError(BaseWebpackLoaderException): 34 | """ 35 | The bundle name was invalid. 36 | """ 37 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_slug": "frontend", 3 | "run_npm_command_at_root": "y", 4 | "style_solution": ["tailwind", "bootstrap", "scss"] 5 | } 6 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import shutil 4 | 5 | TERMINATOR = "\x1b[0m" 6 | WARNING = "\x1b[1;33m [WARNING]: " 7 | INFO = "\x1b[1;33m [INFO]: " 8 | HINT = "\x1b[3;33m" 9 | SUCCESS = "\x1b[1;32m [SUCCESS]: " 10 | 11 | DENY_LIST = [".gitignore"] 12 | ALLOW_LIST = ["package.json", "package-lock.json", "postcss.config.js"] 13 | 14 | 15 | def print_success_msg(msg): 16 | print(SUCCESS + msg + TERMINATOR) 17 | 18 | 19 | def get_frontend_config_files(): 20 | frontend_path = os.getcwd() 21 | 22 | for f in os.listdir(frontend_path): 23 | if f.startswith(".") and f not in DENY_LIST: 24 | full_path = os.path.join(frontend_path, f) 25 | yield os.path.dirname(full_path), os.path.basename(full_path) 26 | 27 | for f in ALLOW_LIST: 28 | full_path = os.path.join(frontend_path, f) 29 | yield os.path.dirname(full_path), os.path.basename(full_path) 30 | 31 | 32 | def process_style_entry_file(): 33 | frontend_path = os.getcwd() 34 | 35 | if "{{ cookiecutter.style_solution }}".lower() == "tailwind": 36 | full_path = os.path.join(frontend_path, "src/styles/index.scss") 37 | os.remove(full_path) 38 | elif "{{ cookiecutter.style_solution }}".lower() in ["scss", "bootstrap"]: 39 | full_path = os.path.join(frontend_path, "src/styles/index.css") 40 | os.remove(full_path) 41 | 42 | 43 | def copy_frontend_config_files(): 44 | """ 45 | Copy frontend config files to the root directory 46 | """ 47 | for dirname, filename in get_frontend_config_files(): 48 | old_full_path = os.path.join(dirname, filename) 49 | root_dir = os.path.dirname(dirname) 50 | new_full_path = os.path.join(root_dir, filename) 51 | shutil.copyfile(old_full_path, new_full_path) 52 | 53 | os.remove(old_full_path) 54 | 55 | 56 | def update_webpack_path(): 57 | """ 58 | Update webpack config file path in package.json 59 | """ 60 | file_path = "package.json" 61 | with open(file_path, "r+") as f: 62 | file_contents = f.read().replace( 63 | "--config webpack/", "--config {{ cookiecutter.project_slug }}/webpack/" 64 | ) 65 | f.seek(0) 66 | f.write(file_contents) 67 | f.truncate() 68 | 69 | 70 | def main(): 71 | if "{{ cookiecutter.run_npm_command_at_root }}".lower() == "y": 72 | update_webpack_path() 73 | copy_frontend_config_files() 74 | 75 | process_style_entry_file() 76 | 77 | print_success_msg( 78 | f"Frontend app '{{ cookiecutter.project_slug }}' " 79 | f"has been created. " 80 | f"To know more, check https://python-webpack-boilerplate.rtfd.io/en/latest/frontend/" 81 | ) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", 7 | "corejs": "3.0.0" 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-transform-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.browserslistrc: -------------------------------------------------------------------------------- 1 | [production staging] 2 | >5% 3 | last 2 versions 4 | not ie > 0 5 | not ie_mob > 0 6 | Firefox ESR 7 | 8 | [development] 9 | last 1 chrome version 10 | last 1 firefox version 11 | last 1 edge version 12 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": [ 4 | "eslint:recommended" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 8, 12 | "sourceType": "module", 13 | "requireConfigFile": false 14 | }, 15 | "rules": { 16 | "semi": 2 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | build 6 | 7 | # misc 8 | .DS_Store 9 | 10 | npm-debug.log 11 | yarn-error.log 12 | yarn.lock 13 | .yarnclean 14 | .vscode 15 | .idea 16 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.nvmrc: -------------------------------------------------------------------------------- 1 | lts/jod 2 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.style_solution == 'tailwind' %} 2 | { 3 | "extends": "stylelint-config-standard", 4 | "rules": { 5 | "at-rule-no-unknown": null 6 | } 7 | } 8 | {% else %} 9 | { 10 | "extends": "stylelint-config-standard-scss", 11 | "rules": { 12 | "at-rule-no-unknown": null, 13 | "scss/at-rule-no-unknown": true, 14 | "scss/at-import-partial-extension": null 15 | } 16 | } 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This project was created with [python-webpack-boilerplate](https://github.com/AccordBox/python-webpack-boilerplate) 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm run start` 10 | 11 | `npm run start` will launch a server process, which makes `live reloading` possible. 12 | 13 | If you change JS or SCSS files, the web page would auto refresh after the change. Now the server is working on port 9091 by default, but you can change it in `webpack/webpack.config.dev.js` 14 | 15 | ### `npm run watch` 16 | 17 | run webpack in `watch` mode. 18 | 19 | ### `npm run build` 20 | 21 | [production mode](https://webpack.js.org/guides/production/), Webpack would focus on minified bundles, lighter weight source maps, and optimized assets to improve load time. 22 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python-webpack-boilerplate", 3 | "version": "1.0.4", 4 | "description": "Webpack boilerplate for Django & Flask", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production webpack --config webpack/webpack.config.prod.js", 7 | "start": "webpack serve --config webpack/webpack.config.dev.js", 8 | "watch": "webpack --watch --config webpack/webpack.config.watch.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/AccordBox/python-webpack-boilerplate" 13 | }, 14 | "keywords": [ 15 | "webpack", 16 | "startkit", 17 | "frontend" 18 | ], 19 | "author": "Michael Yin", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/AccordBox/python-webpack-boilerplate/issues" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.20.5", 26 | "@babel/eslint-parser": "^7.26.5", 27 | "@babel/plugin-transform-class-properties": "^7.25.9", 28 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 29 | "@babel/preset-env": "^7.26.7", 30 | "babel-loader": "^9.2.1", 31 | "clean-webpack-plugin": "^4.0.0", 32 | "copy-webpack-plugin": "^12.0.2", 33 | "cross-env": "^7.0.3", 34 | "css-loader": "^7.1.2", 35 | "eslint": "^9.20.1", 36 | "eslint-webpack-plugin": "^4.2.0", 37 | "mini-css-extract-plugin": "^2.9.2", 38 | "postcss-loader": "^8.0.0", 39 | "postcss-preset-env": "^10.0.3", 40 | {% if cookiecutter.style_solution == 'scss' %} 41 | "sass": "1.77.6", 42 | "sass-loader": "^16.0.1", 43 | "stylelint-config-standard-scss": "^14.0.0", 44 | {% elif cookiecutter.style_solution == 'tailwind' %} 45 | "tailwindcss": "^4.0.3", 46 | "@tailwindcss/postcss": "^4.0.3", 47 | {% elif cookiecutter.style_solution == 'bootstrap' %} 48 | "sass": "1.77.6", 49 | "sass-loader": "^16.0.1", 50 | "stylelint-config-standard-scss": "^14.0.0", 51 | "bootstrap": "^5.3.3", 52 | {% endif %} 53 | "style-loader": "^4.0.0", 54 | "stylelint": "^16.14.1", 55 | "stylelint-config-standard": "^37.0.0", 56 | "stylelint-webpack-plugin": "^5.0.1", 57 | "webpack": "^5.97.1", 58 | "webpack-assets-manifest": "^5.2.1", 59 | "webpack-cli": "^6.0.1", 60 | "webpack-dev-server": "^5.2.0", 61 | "webpack-merge": "^6.0.1" 62 | }, 63 | "dependencies": { 64 | "core-js": "^3.37.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/postcss.config.js: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.style_solution == 'tailwind' %} 2 | module.exports = { 3 | plugins: { 4 | "@tailwindcss/postcss": {}, 5 | } 6 | } 7 | {% else %} 8 | const postcssPresetEnv = require("postcss-preset-env"); 9 | 10 | module.exports = { 11 | plugins: [postcssPresetEnv()], 12 | }; 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/application/README.md: -------------------------------------------------------------------------------- 1 | When Webpacker compiles your JavaScript code, it scans the `src/application` directory for files with the .js extension and automatically includes them as entry points for the Webpack bundling process. 2 | 3 | For the `application/app.js`, you can import it in template like this: 4 | 5 | ``` 6 | {% raw %}{% javascript_pack 'app' attrs='charset="UTF-8"' %}{% endraw %} 7 | ``` 8 | 9 | In most cases, you do not need to create another file in this directory. 10 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/application/app.js: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.style_solution == 'scss' %} 2 | // This is the style entry file 3 | import "../styles/index.scss"; 4 | 5 | // We can import other JS file as we like 6 | import Jumbotron from "../components/jumbotron"; 7 | 8 | window.document.addEventListener("DOMContentLoaded", function () { 9 | window.console.log("dom ready"); 10 | 11 | // Find elements and initialize 12 | for (const elem of document.querySelectorAll(Jumbotron.selector())) { 13 | new Jumbotron(elem); 14 | } 15 | }); 16 | {% elif cookiecutter.style_solution == 'tailwind' %} 17 | // This is the style entry file 18 | import "../styles/index.css"; 19 | 20 | // We can import other JS file as we like 21 | import Jumbotron from "../components/jumbotron"; 22 | 23 | window.document.addEventListener("DOMContentLoaded", function () { 24 | window.console.log("dom ready"); 25 | 26 | // Find elements and initialize 27 | for (const elem of document.querySelectorAll(Jumbotron.selector())) { 28 | new Jumbotron(elem); 29 | } 30 | }); 31 | {% elif cookiecutter.style_solution == 'bootstrap' %} 32 | // This is the style entry file 33 | import "../styles/index.scss"; 34 | 35 | import "bootstrap/dist/js/bootstrap.bundle"; 36 | 37 | // We can import other JS file as we like 38 | import Jumbotron from "../components/jumbotron"; 39 | 40 | window.document.addEventListener("DOMContentLoaded", function () { 41 | window.console.log("dom ready"); 42 | 43 | // Find elements and initialize 44 | for (const elem of document.querySelectorAll(Jumbotron.selector())) { 45 | new Jumbotron(elem); 46 | } 47 | }); 48 | {% endif %} 49 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/components/README.md: -------------------------------------------------------------------------------- 1 | We recommend write Javascript here to make your code reusable. 2 | 3 | And you can check below packages to increase your productivity: 4 | 5 | 1. [Stimulus](https://stimulus.hotwired.dev/) 6 | 2. [Alpine.js](https://alpinejs.dev/) 7 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/components/jumbotron.js: -------------------------------------------------------------------------------- 1 | class Jumbotron { 2 | static selector() { 3 | return "[data-jumbotron]"; 4 | } 5 | 6 | constructor(node) { 7 | this.node = node; 8 | console.log(`Jumbotron initialized for node: ${node}`); 9 | // do something here 10 | } 11 | } 12 | 13 | export default Jumbotron; 14 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | .jumbotron { 4 | /*should be relative path of the entry css file*/ 5 | background-image: url("../../vendors/images/sample.jpg"); 6 | background-size: cover; 7 | } 8 | 9 | @layer components{ 10 | .btn-blue { 11 | @apply inline-flex items-center; 12 | @apply px-4 py-2; 13 | @apply font-semibold rounded-lg shadow-md; 14 | @apply text-white bg-blue-500; 15 | @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/50; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.style_solution == 'bootstrap' %} 2 | // If you comment out code below, bootstrap will use red as primary color 3 | // and btn-primary will become red 4 | 5 | // $primary: red; 6 | 7 | @import "~bootstrap/scss/bootstrap"; 8 | 9 | .jumbotron { 10 | // should be relative path of the entry scss file 11 | background-image: url("../../vendors/images/sample.jpg"); 12 | background-size: cover; 13 | } 14 | {% elif cookiecutter.style_solution == 'scss' %} 15 | .jumbotron { 16 | // should be relative path of the entry scss file 17 | background-image: url("../../vendors/images/sample.jpg"); 18 | background-size: cover; 19 | } 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/.gitkeep -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/.gitkeep -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/sample.jpg -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/vendors/images/webpack.png -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const glob = require("glob"); 2 | const Path = require("path"); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | const WebpackAssetsManifest = require("webpack-assets-manifest"); 6 | 7 | const getEntryObject = () => { 8 | const entries = {}; 9 | // for javascript/typescript entry file 10 | glob 11 | .sync(Path.join(__dirname, "../src/application/*.{js,ts}")) 12 | .forEach((path) => { 13 | const name = Path.basename(path); 14 | const extension = Path.extname(path); 15 | const entryName = name.replace(extension, ""); 16 | if (entryName in entries) { 17 | throw new Error(`Entry file conflict: ${entryName}`); 18 | } 19 | entries[entryName] = path; 20 | }); 21 | return entries; 22 | }; 23 | 24 | module.exports = { 25 | entry: getEntryObject(), 26 | output: { 27 | path: Path.join(__dirname, "../build"), 28 | filename: "js/[name].js", 29 | publicPath: "/static/", 30 | assetModuleFilename: "[path][name][ext]", 31 | }, 32 | optimization: { 33 | splitChunks: { 34 | chunks: "all", 35 | }, 36 | 37 | runtimeChunk: "single", 38 | }, 39 | plugins: [ 40 | new CleanWebpackPlugin(), 41 | new CopyWebpackPlugin({ 42 | patterns: [ 43 | { from: Path.resolve(__dirname, "../vendors"), to: "vendors" }, 44 | ], 45 | }), 46 | new WebpackAssetsManifest({ 47 | entrypoints: true, 48 | output: "manifest.json", 49 | writeToDisk: true, 50 | publicPath: true, 51 | }), 52 | ], 53 | resolve: { 54 | alias: { 55 | "~": Path.resolve(__dirname, "../src"), 56 | }, 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.mjs$/, 62 | include: /node_modules/, 63 | type: "javascript/auto", 64 | }, 65 | { 66 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/, 67 | type: "asset", 68 | }, 69 | ], 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const Path = require("path"); 2 | const Webpack = require("webpack"); 3 | const { merge } = require("webpack-merge"); 4 | const StylelintPlugin = require("stylelint-webpack-plugin"); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const ESLintPlugin = require("eslint-webpack-plugin"); 7 | 8 | const common = require("./webpack.common.js"); 9 | 10 | module.exports = merge(common, { 11 | target: "web", 12 | mode: "development", 13 | devtool: "inline-source-map", 14 | output: { 15 | chunkFilename: "js/[name].chunk.js", 16 | publicPath: "http://localhost:9091/", 17 | }, 18 | devServer: { 19 | hot: true, 20 | host: "0.0.0.0", 21 | port: 9091, 22 | headers: { 23 | "Access-Control-Allow-Origin": "*", 24 | }, 25 | devMiddleware: { 26 | writeToDisk: true, 27 | }, 28 | }, 29 | plugins: [ 30 | new Webpack.DefinePlugin({ 31 | "process.env.NODE_ENV": JSON.stringify("development"), 32 | }), 33 | new StylelintPlugin({ 34 | files: Path.resolve(__dirname, "../src/**/*.s?(a|c)ss"), 35 | }), 36 | new ESLintPlugin({ 37 | extensions: "js", 38 | emitWarning: true, 39 | files: Path.resolve(__dirname, "../src"), 40 | }), 41 | new MiniCssExtractPlugin({ 42 | filename: "css/[name].css", 43 | chunkFilename: "css/[id].css", 44 | }), 45 | ], 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.html$/i, 50 | loader: "html-loader", 51 | }, 52 | { 53 | test: /\.js$/, 54 | include: Path.resolve(__dirname, "../src"), 55 | loader: "babel-loader", 56 | }, 57 | { 58 | test: /\.s?css$/i, 59 | use: [ 60 | MiniCssExtractPlugin.loader, 61 | { 62 | loader: "css-loader", 63 | options: { 64 | sourceMap: true, 65 | }, 66 | }, 67 | "postcss-loader", 68 | {% if cookiecutter.style_solution == 'tailwind' %} 69 | {% else %} 70 | "sass-loader", 71 | {% endif %} 72 | ], 73 | }, 74 | ], 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const Webpack = require("webpack"); 2 | const { merge } = require("webpack-merge"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const common = require("./webpack.common.js"); 5 | 6 | module.exports = merge(common, { 7 | mode: "production", 8 | devtool: "source-map", 9 | bail: true, 10 | output: { 11 | filename: "js/[name].[chunkhash:8].js", 12 | chunkFilename: "js/[name].[chunkhash:8].chunk.js", 13 | }, 14 | plugins: [ 15 | new Webpack.DefinePlugin({ 16 | "process.env.NODE_ENV": JSON.stringify("production"), 17 | }), 18 | new MiniCssExtractPlugin({ 19 | filename: "css/[name].[contenthash].css", 20 | chunkFilename: "css/[id].[contenthash].css", 21 | }), 22 | ], 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: "babel-loader", 29 | }, 30 | { 31 | test: /\.s?css/i, 32 | use: [ 33 | MiniCssExtractPlugin.loader, 34 | "css-loader", 35 | "postcss-loader", 36 | {% if cookiecutter.style_solution == 'tailwind' %} 37 | {% else %} 38 | "sass-loader", 39 | {% endif %} 40 | ], 41 | }, 42 | ], 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /webpack_boilerplate/frontend_template/{{cookiecutter.project_slug}}/webpack/webpack.config.watch.js: -------------------------------------------------------------------------------- 1 | const Path = require("path"); 2 | const Webpack = require("webpack"); 3 | const { merge } = require("webpack-merge"); 4 | const StylelintPlugin = require("stylelint-webpack-plugin"); 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | const ESLintPlugin = require("eslint-webpack-plugin"); 7 | 8 | const common = require("./webpack.common.js"); 9 | 10 | module.exports = merge(common, { 11 | target: "web", 12 | mode: "development", 13 | devtool: "inline-source-map", 14 | output: { 15 | chunkFilename: "js/[name].chunk.js", 16 | }, 17 | plugins: [ 18 | new Webpack.DefinePlugin({ 19 | "process.env.NODE_ENV": JSON.stringify("development"), 20 | }), 21 | new StylelintPlugin({ 22 | files: Path.resolve(__dirname, "../src/**/*.s?(a|c)ss"), 23 | }), 24 | new ESLintPlugin({ 25 | extensions: "js", 26 | emitWarning: true, 27 | files: Path.resolve(__dirname, "../src"), 28 | }), 29 | new MiniCssExtractPlugin({ 30 | filename: "css/[name].css", 31 | chunkFilename: "css/[id].css", 32 | }), 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.html$/i, 38 | loader: "html-loader", 39 | }, 40 | { 41 | test: /\.js$/, 42 | include: Path.resolve(__dirname, "../src"), 43 | loader: "babel-loader", 44 | }, 45 | { 46 | test: /\.s?css$/i, 47 | use: [ 48 | MiniCssExtractPlugin.loader, 49 | { 50 | loader: "css-loader", 51 | options: { 52 | sourceMap: true, 53 | }, 54 | }, 55 | "postcss-loader", 56 | {% if cookiecutter.style_solution == 'tailwind' %} 57 | {% else %} 58 | "sass-loader", 59 | {% endif %} 60 | ], 61 | }, 62 | ], 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /webpack_boilerplate/loader.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import time 4 | from io import open 5 | 6 | from .exceptions import ( 7 | WebpackBundleLookupError, 8 | WebpackError, 9 | WebpackLoaderBadStatsError, 10 | WebpackLoaderTimeoutError, 11 | ) 12 | 13 | 14 | class WebpackLoader(object): 15 | _assets = {} 16 | 17 | def __init__(self, name, config): 18 | self.name = name 19 | self.config = config 20 | 21 | def load_assets(self): 22 | # TODO 23 | # poll when debugging and block request until bundle is compiled 24 | # or the build times out 25 | try: 26 | with open(self.config["MANIFEST_FILE"], encoding="utf-8") as f: 27 | return json.load(f) 28 | except IOError: 29 | raise IOError( 30 | "Error reading {0}. Are you sure webpack has generated " 31 | "the file and the path is correct?".format(self.config["MANIFEST_FILE"]) 32 | ) 33 | 34 | def get_assets(self): 35 | if self.config["CACHE"]: 36 | if self.name not in self._assets: 37 | self._assets[self.name] = self.load_assets() 38 | return self._assets[self.name] 39 | return self.load_assets() 40 | 41 | def filter_chunks(self, chunks): 42 | for chunk in chunks: 43 | ignore = any(regex.match(chunk["url"]) for regex in self.config["ignores"]) 44 | if not ignore: 45 | chunk["url"] = self.get_chunk_url(chunk) 46 | yield chunk 47 | 48 | def get_chunk_url(self, chunk): 49 | url = chunk["url"] 50 | 51 | if self.config.get("web_framework", None) == "django": 52 | from django.contrib.staticfiles.storage import staticfiles_storage 53 | from django.conf import settings 54 | 55 | if url.startswith("http"): 56 | # webpack dev server 57 | return url 58 | else: 59 | prefix = settings.STATIC_URL 60 | url_without_static_prefix = url[ 61 | url.startswith(prefix) and len(prefix) : 62 | ] 63 | return staticfiles_storage.url(url_without_static_prefix) 64 | else: 65 | return url 66 | 67 | def get_bundle(self, bundle_name): 68 | assets = copy.copy(self.get_assets()) 69 | try: 70 | # keep the order 71 | js = assets["entrypoints"][bundle_name]["assets"].get("js", []) 72 | css = assets["entrypoints"][bundle_name]["assets"].get("css", []) 73 | js_css = js + css 74 | 75 | assets.pop("entrypoints") 76 | # so url is the key 77 | reversed_assets = {value: key for (key, value) in assets.items()} 78 | chunks = [{"name": reversed_assets[url], "url": url,} for url in js_css] 79 | except Exception: 80 | raise WebpackBundleLookupError( 81 | "Cannot resolve bundle {0}.".format(bundle_name) 82 | ) 83 | 84 | return self.filter_chunks(chunks) 85 | -------------------------------------------------------------------------------- /webpack_boilerplate/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/management/__init__.py -------------------------------------------------------------------------------- /webpack_boilerplate/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/management/commands/__init__.py -------------------------------------------------------------------------------- /webpack_boilerplate/management/commands/webpack_init.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | 6 | class Command(BaseCommand): 7 | def add_arguments(self, parser): 8 | parser.add_argument( 9 | "--no-input", action="store_true", help="Do not ask for input.", 10 | ) 11 | 12 | def handle(self, *args, **options): 13 | from cookiecutter.main import cookiecutter 14 | 15 | pkg_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 16 | if options["no_input"]: 17 | cookiecutter(pkg_path, directory="frontend_template", no_input=True) 18 | else: 19 | cookiecutter(pkg_path, directory="frontend_template") 20 | -------------------------------------------------------------------------------- /webpack_boilerplate/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AccordBox/python-webpack-boilerplate/3e6acbaca706817d5c01647234c686ef23acefab/webpack_boilerplate/templatetags/__init__.py -------------------------------------------------------------------------------- /webpack_boilerplate/templatetags/webpack_loader.py: -------------------------------------------------------------------------------- 1 | from django import VERSION, template 2 | from django.utils.safestring import mark_safe 3 | 4 | from .. import utils 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag 10 | def render_bundle(bundle_name, extension=None, config="DEFAULT", attrs=""): 11 | tags = utils.get_as_tags( 12 | bundle_name, extension=extension, config=config, attrs=attrs 13 | ) 14 | return mark_safe("\n".join(tags)) 15 | 16 | 17 | @register.simple_tag 18 | def webpack_static(asset_name, config="DEFAULT"): 19 | return utils.get_static(asset_name, config=config) 20 | 21 | 22 | assignment_tag = register.simple_tag if VERSION >= (1, 9) else register.assignment_tag 23 | 24 | 25 | @assignment_tag 26 | def get_files(bundle_name, extension=None, config="DEFAULT"): 27 | """ 28 | Returns all chunks in the given bundle. 29 | Example usage:: 30 | 31 | {% get_files 'editor' 'css' as editor_css_chunks %} 32 | CKEDITOR.config.contentsCss = '{{ editor_css_chunks.0.publicPath }}'; 33 | 34 | :param bundle_name: The name of the bundle 35 | :param extension: (optional) filter by extension 36 | :param config: (optional) the name of the configuration 37 | :return: a list of matching chunks 38 | """ 39 | return utils.get_files(bundle_name, extension=extension, config=config) 40 | 41 | 42 | @register.simple_tag 43 | def javascript_pack(*names, **kwargs): 44 | tags = [] 45 | for name in names: 46 | sub_tags = utils.get_as_tags( 47 | name, extension="js", attrs=kwargs.get("attrs", "") 48 | ) 49 | for sub_tag in sub_tags: 50 | if sub_tag not in tags: 51 | tags.append(sub_tag) 52 | return mark_safe("\n".join(tags)) 53 | 54 | 55 | @register.simple_tag 56 | def stylesheet_pack(*names, **kwargs): 57 | tags = [] 58 | for name in names: 59 | sub_tags = utils.get_as_tags( 60 | name, extension="css", attrs=kwargs.get("attrs", "") 61 | ) 62 | for sub_tag in sub_tags: 63 | if sub_tag not in tags: 64 | tags.append(sub_tag) 65 | return mark_safe("\n".join(tags)) 66 | 67 | 68 | @register.simple_tag 69 | def asset_pack_url(asset_name): 70 | return utils.get_static(asset_name) 71 | -------------------------------------------------------------------------------- /webpack_boilerplate/utils.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from .config import get_setting_value, load_config 4 | 5 | _loaders = {} 6 | 7 | 8 | def import_string(dotted_path): 9 | """ 10 | This is a rough copy of django's import_string, which wasn't introduced until Django 1.7 11 | 12 | Once this package's support for Django 1.6 has been removed, this can be safely replaced with 13 | `from django.utils.module_loading import import_string` 14 | """ 15 | try: 16 | module_path, class_name = dotted_path.rsplit(".", 1) 17 | module = import_module(module_path) 18 | return getattr(module, class_name) 19 | except (ValueError, AttributeError, ImportError): 20 | raise ImportError("%s doesn't look like a valid module path" % dotted_path) 21 | 22 | 23 | def get_loader(config_name): 24 | if config_name not in _loaders: 25 | config = load_config(config_name) 26 | loader_class = import_string(config["LOADER_CLASS"]) 27 | _loaders[config_name] = loader_class(config_name, config) 28 | return _loaders[config_name] 29 | 30 | 31 | def _filter_by_extension(bundle, extension): 32 | """Return only files with the given extension""" 33 | for chunk in bundle: 34 | if chunk["name"].endswith(".{0}".format(extension)): 35 | yield chunk 36 | 37 | 38 | def _get_bundle(bundle_name, extension, config): 39 | bundle = list(get_loader(config).get_bundle(bundle_name)) 40 | if extension: 41 | bundle = _filter_by_extension(bundle, extension) 42 | return bundle 43 | 44 | 45 | def get_files(bundle_name, extension=None, config="DEFAULT"): 46 | """Returns list of chunks from named bundle""" 47 | return list(_get_bundle(bundle_name, extension, config)) 48 | 49 | 50 | def get_as_tags(bundle_name, extension=None, config="DEFAULT", attrs=""): 51 | """ 52 | Get a list of formatted ').format( 67 | chunk["url"], attrs 68 | ) 69 | ) 70 | elif chunk["name"].endswith((".css", ".css.gz")): 71 | tags.append( 72 | ('').format( 73 | chunk["url"], attrs 74 | ) 75 | ) 76 | return tags 77 | 78 | 79 | def get_static(asset_name, config="DEFAULT"): 80 | """ 81 | Equivalent to Django's 'static' look up but for webpack assets. 82 | """ 83 | return "{0}{1}".format(get_setting_value("STATIC_URL"), asset_name) 84 | --------------------------------------------------------------------------------