Skip to content

Commit 4d3068f

Browse files
committed
Adds support for Tailwind CSS v4+
1 parent c4261a7 commit 4d3068f

File tree

29 files changed

+151
-113
lines changed

29 files changed

+151
-113
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ repos:
3535
- flake8-comprehensions
3636
- flake8-tidy-imports
3737
- flake8-typing-imports
38-
exclude: src/tailwind/app_template/{{cookiecutter.app_name}}/apps.py
38+
exclude: 'src/tailwind/app_template_v3/{{cookiecutter.app_name}}/apps.py|src/tailwind/app_template_v4/{{cookiecutter.app_name}}/apps.py'

docs/index.md

-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ Contents
1717
* [Settings](settings.md)
1818
* [Template tags](templatetags.md)
1919
* [Migrating from browser-sync to django-browser-reload](django_browser_reload.md)
20-
* [Upgrading from v2 to v3](upgrade.md)
2120
* [Updating Tailwind CSS and its dependencies](updating.md)
2221
* [Running in docker](docker.md)

docs/upgrade.md

-37
This file was deleted.

example/theme/static_src/package.json

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
{
22
"name": "theme",
3-
"version": "3.9.0",
3+
"version": "4.0.0",
44
"description": "",
55
"scripts": {
66
"start": "npm run dev",
77
"build": "npm run build:clean && npm run build:tailwind",
88
"build:clean": "rimraf ../static/css/dist",
9-
"build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify",
10-
"dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w",
11-
"tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js"
9+
"build:tailwind": "cross-env NODE_ENV=production postcss ./src/styles.css -o ../static/css/dist/styles.css --minify",
10+
"dev": "cross-env NODE_ENV=development postcss ./src/styles.css -o ../static/css/dist/styles.css --watch"
1211
},
1312
"keywords": [],
1413
"author": "",
1514
"license": "MIT",
1615
"devDependencies": {
17-
"@tailwindcss/aspect-ratio": "^0.4.2",
18-
"@tailwindcss/forms": "^0.5.10",
19-
"@tailwindcss/typography": "^0.5.16",
16+
"@tailwindcss/postcss": "^4.1.0",
2017
"cross-env": "^7.0.3",
2118
"postcss": "^8.5.3",
22-
"postcss-import": "^16.1.0",
19+
"postcss-cli": "^11.0.1",
2320
"postcss-nested": "^7.0.2",
2421
"postcss-simple-vars": "^7.0.1",
2522
"rimraf": "^6.0.1",
26-
"tailwindcss": "^3.4.17"
23+
"tailwindcss": "^4.1.0"
2724
}
2825
}

example/theme/static_src/postcss.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
plugins: {
3-
"postcss-import": {},
3+
"@tailwindcss/postcss": {},
44
"postcss-simple-vars": {},
55
"postcss-nested": {}
66
},
+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1-
@tailwind base;
2-
@tailwind components;
3-
@tailwind utilities;
1+
@import "tailwindcss";
2+
3+
/**
4+
* A catch-all path to Django template files, JavaScript, and Python files
5+
* that contain Tailwind CSS classes and will be scanned by Tailwind to generate the final CSS file.
6+
*
7+
* If your final CSS file is not being updated after code changes, you may want to broaden the scope of this path.
8+
*/
9+
@source "../../**/*.{html,py,js}";

src/tailwind/app_template/{{cookiecutter.app_name}}/static_src/tailwind.config.js

-57
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"app_name": "",
3+
"_copy_without_render": [
4+
"templates/base.html"
5+
]
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import re
2+
import sys
3+
4+
APP_NAME_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$"
5+
6+
app_name = "{{ cookiecutter.app_name }}"
7+
8+
if not re.match(APP_NAME_REGEX, app_name):
9+
print(f"ERROR: {app_name} is not a valid Django app name!")
10+
11+
# exits with status 1 to indicate failure
12+
sys.exit(1)

src/tailwind/app_template_v4/{{cookiecutter.app_name}}/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class {{ cookiecutter.app_name[0]|upper }}{{ cookiecutter.app_name[1:] }}Config(AppConfig):
5+
name = '{{ cookiecutter.app_name }}'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "{{ cookiecutter.app_name }}",
3+
"version": "4.0.0",
4+
"description": "",
5+
"scripts": {
6+
"start": "npm run dev",
7+
"build": "npm run build:clean && npm run build:tailwind",
8+
"build:clean": "rimraf ../static/css/dist",
9+
"build:tailwind": "cross-env NODE_ENV=production postcss ./src/styles.css -o ../static/css/dist/styles.css --minify",
10+
"dev": "cross-env NODE_ENV=development postcss ./src/styles.css -o ../static/css/dist/styles.css --watch"
11+
},
12+
"keywords": [],
13+
"author": "",
14+
"license": "MIT",
15+
"devDependencies": {
16+
"@tailwindcss/postcss": "^4.1.0",
17+
"cross-env": "^7.0.3",
18+
"postcss": "^8.5.3",
19+
"postcss-cli": "^11.0.1",
20+
"postcss-nested": "^7.0.2",
21+
"postcss-simple-vars": "^7.0.1",
22+
"rimraf": "^6.0.1",
23+
"tailwindcss": "^4.1.0"
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
plugins: {
3+
"@tailwindcss/postcss": {},
4+
"postcss-simple-vars": {},
5+
"postcss-nested": {}
6+
},
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import "tailwindcss";
2+
3+
/**
4+
* A catch-all path to Django template files, JavaScript, and Python files
5+
* that contain Tailwind CSS classes and will be scanned by Tailwind to generate the final CSS file.
6+
*
7+
* If your final CSS file is not being updated after code changes, you may want to broaden the scope of this path.
8+
*/
9+
@source "../../**/*.{html,py,js}";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% load static tailwind_tags %}
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<title>Django Tailwind</title>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
9+
{% tailwind_css %}
10+
</head>
11+
12+
<body class="bg-gray-50 font-serif leading-normal tracking-normal">
13+
<div class="container mx-auto">
14+
<section class="flex items-center justify-center h-screen">
15+
<h1 class="text-5xl">Django + Tailwind = ❤️</h1>
16+
</section>
17+
</div>
18+
</body>
19+
</html>

src/tailwind/management/commands/tailwind.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ def add_arguments(self, parser):
3636
action="store_true",
3737
help="Initializes Tailwind project without user prompts",
3838
)
39+
parser.add_argument(
40+
"--tailwind-version",
41+
default="4",
42+
choices=["3", "4"],
43+
help="Specifies the Tailwind version to install",
44+
)
3945
parser.add_argument(
4046
"--app-name",
4147
help="Sets default app name on Tailwind project initialization",
@@ -78,7 +84,7 @@ def handle_init_command(self, **options):
7884
app_path = cookiecutter(
7985
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
8086
output_dir=os.getcwd(),
81-
directory="app_template",
87+
directory=f"app_template_v{options['tailwind_version']}",
8288
no_input=options["no_input"],
8389
overwrite_if_exists=False,
8490
extra_context={"app_name": options["app_name"].strip() if options.get("app_name") else "theme"},

src/tailwind/validate.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def is_installed(self, app_name):
2828
raise ValidationError(f"{app_name} is not in INSTALLED_APPS")
2929

3030
def is_tailwind_app(self, app_name):
31-
if not os.path.isfile(os.path.join(get_tailwind_src_path(app_name), "tailwind.config.js")):
31+
if not os.path.isfile(os.path.join(get_tailwind_src_path(app_name), "package.json")):
3232
raise ValidationError(f"'{app_name}' isn't a Tailwind app")
3333

3434
def has_settings(self):

tests/test_cli.py

+42-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111

1212
@pytest.mark.parametrize("no_package_lock", [True, False])
13-
def test_tailwind_install_and_build(no_package_lock, settings):
13+
def test_tailwind_install_and_build_v3(no_package_lock, settings):
1414
app_name = f'test_theme_{str(uuid.uuid1()).replace("-", "_")}'
1515

16-
call_command("tailwind", "init", "--app-name", app_name, "--no-input")
16+
call_command("tailwind", "init", "--app-name", app_name, "--no-input", "--tailwind-version", "3")
1717

1818
settings.INSTALLED_APPS += [app_name]
1919
settings.TAILWIND_APP_NAME = app_name
@@ -47,3 +47,43 @@ def test_tailwind_install_and_build(no_package_lock, settings):
4747
), "Tailwind has built a css/styles.css file"
4848

4949
cleanup_theme_app_dir(app_name)
50+
51+
52+
@pytest.mark.parametrize("no_package_lock", [True, False])
53+
def test_tailwind_install_and_build_v4(no_package_lock, settings):
54+
app_name = f'test_theme_{str(uuid.uuid1()).replace("-", "_")}'
55+
56+
call_command("tailwind", "init", "--app-name", app_name, "--no-input")
57+
58+
settings.INSTALLED_APPS += [app_name]
59+
settings.TAILWIND_APP_NAME = app_name
60+
61+
assert os.path.isfile(os.path.join(get_app_path(app_name), "apps.py")), 'The "theme" app has been generated'
62+
63+
tailwind_config_path = os.path.join(get_app_path(app_name), "static_src", "tailwind.config.js")
64+
assert not os.path.isfile(tailwind_config_path), "tailwind.config.js is absent from tailwind v4"
65+
66+
if no_package_lock:
67+
call_command("tailwind", "install", "--no-package-lock")
68+
else:
69+
call_command("tailwind", "install")
70+
71+
package_json_path = os.path.join(get_app_path(app_name), "static_src", "package.json")
72+
assert os.path.isfile(package_json_path), "Tailwind has created package.json file"
73+
74+
package_lock_json_path = os.path.join(get_app_path(app_name), "static_src", "package-lock.json")
75+
if no_package_lock:
76+
assert not os.path.isfile(package_lock_json_path), "Tailwind has not created package-lock.json file"
77+
else:
78+
assert os.path.isfile(package_lock_json_path), "Tailwind has created package-lock.json file"
79+
80+
assert os.path.isdir(
81+
os.path.join(get_app_path(app_name), "static_src", "node_modules")
82+
), "Tailwind has been installed from npm"
83+
84+
call_command("tailwind", "build")
85+
assert os.path.isfile(
86+
os.path.join(get_app_path(app_name), "static", "css", "dist", "styles.css")
87+
), "Tailwind has built a css/styles.css file"
88+
89+
cleanup_theme_app_dir(app_name)

0 commit comments

Comments
 (0)