plain.tailwind
Integrate Tailwind CSS without JavaScript or npm.
Made possible by the Tailwind standalone CLI, which is installed for you.
$ plain tailwind
Usage: plain tailwind [OPTIONS] COMMAND [ARGS]...
Tailwind CSS
Options:
--help Show this message and exit.
Commands:
compile Compile a Tailwind CSS file
init Install Tailwind, create a tailwind.config.js...
update Update the Tailwind CSS version
Installation
Add plain.tailwind
to your INSTALLED_PACKAGES
:
# settings.py
INSTALLED_PACKAGES = [
# ...
"plain.tailwind",
]
Create a new tailwind.config.js
file in your project root:
plain tailwind init
This will also create a tailwind.css
file at static/src/tailwind.css
where additional CSS can be added.
You can customize where these files are located if you need to,
but this is the default (requires STATICFILES_DIR = BASE_DIR / "static"
).
The src/tailwind.css
file is then compiled into dist/tailwind.css
by running tailwind compile
:
plain tailwind compile
When you're working locally, add --watch
to automatically compile as changes are made:
plain tailwind compile --watch
Then include the compiled CSS in your base template <head>
:
{% tailwind_css %}
In your repo you will notice a new .plain
directory that contains tailwind
(the standalone CLI binary) and tailwind.version
(to track the version currently installed).
You should add .plain
to your .gitignore
file.
Updating Tailwind
This package manages the Tailwind versioning by comparing the value in your pyproject.toml
to .plain/tailwind.version
.
# pyproject.toml
[tool.plain.tailwind]
version = "3.4.1"
When you run tailwind compile
,
it will automatically check whether your local installation needs to be updated and will update it if necessary.
You can use the update
command to update your project to the latest version of Tailwind:
plain tailwind update
Adding custom CSS
If you need to actually write some CSS,
it should be done in app/static/src/tailwind.css
.
@tailwind base;
@tailwind components;
/* Add your own "components" here */
.btn {
@apply bg-blue-500 hover:bg-blue-700 text-white;
}
@tailwind utilities;
/* Add your own "utilities" here */
.bg-pattern-stars {
background-image: url("/static/images/stars.png");
}
Read the Tailwind docs for more about using custom styles →
Deployment
If possible, you should add static/dist/tailwind.css
to your .gitignore
and run the plain tailwind compile --minify
command as a part of your deployment pipeline.
When you run plain tailwind compile
, it will automatically check whether the Tailwind standalone CLI has been installed, and install it if it isn't.
When using Plain on Heroku, we do this for you automatically in our Plain buildpack.
1import os
2import platform
3import subprocess
4import sys
5
6import requests
7import tomlkit
8
9from plain.runtime import settings
10
11DEFAULT_CSS = """@tailwind base;
12
13
14@tailwind components;
15
16
17@tailwind utilities;
18"""
19
20
21class Tailwind:
22 @property
23 def target_directory(self) -> str:
24 return str(settings.PLAIN_TEMP_PATH)
25
26 @property
27 def standalone_path(self) -> str:
28 return os.path.join(self.target_directory, "tailwind")
29
30 @property
31 def version_lockfile_path(self) -> str:
32 return os.path.join(self.target_directory, "tailwind.version")
33
34 @property
35 def config_path(self) -> str:
36 return os.path.join(
37 os.path.dirname(self.target_directory), "tailwind.config.js"
38 )
39
40 @property
41 def src_css_path(self) -> str:
42 return settings.TAILWIND_SRC_PATH
43
44 @property
45 def dist_css_path(self) -> str:
46 return settings.TAILWIND_DIST_PATH
47
48 def invoke(self, *args, cwd=None) -> None:
49 result = subprocess.run([self.standalone_path] + list(args), cwd=cwd)
50 if result.returncode != 0:
51 sys.exit(result.returncode)
52
53 def is_installed(self) -> bool:
54 if not os.path.exists(self.target_directory):
55 os.mkdir(self.target_directory)
56 return os.path.exists(os.path.join(self.target_directory, "tailwind"))
57
58 def config_exists(self) -> bool:
59 return os.path.exists(self.config_path)
60
61 def create_config(self):
62 self.invoke("init", self.config_path)
63
64 def src_css_exists(self) -> bool:
65 return os.path.exists(self.src_css_path)
66
67 def create_src_css(self):
68 os.makedirs(os.path.dirname(self.src_css_path), exist_ok=True)
69 with open(self.src_css_path, "w") as f:
70 f.write(DEFAULT_CSS)
71
72 def needs_update(self) -> bool:
73 if not os.path.exists(self.version_lockfile_path):
74 return True
75
76 with open(self.version_lockfile_path) as f:
77 locked_version = f.read().strip()
78
79 if locked_version != self.get_version_from_config():
80 return True
81
82 return False
83
84 def get_version_from_config(self) -> str:
85 pyproject_path = os.path.join(
86 os.path.dirname(self.target_directory), "pyproject.toml"
87 )
88
89 if not os.path.exists(pyproject_path):
90 return ""
91
92 with open(pyproject_path) as f:
93 config = tomlkit.load(f)
94 return (
95 config.get("tool", {})
96 .get("plain", {})
97 .get("tailwind", {})
98 .get("version", "")
99 )
100
101 def set_version_in_config(self, version):
102 pyproject_path = os.path.join(
103 os.path.dirname(self.target_directory), "pyproject.toml"
104 )
105
106 with open(pyproject_path) as f:
107 config = tomlkit.load(f)
108
109 config.setdefault("tool", {}).setdefault("plain", {}).setdefault(
110 "tailwind", {}
111 )["version"] = version
112
113 with open(pyproject_path, "w") as f:
114 tomlkit.dump(config, f)
115
116 def download(self, version="") -> str:
117 if version:
118 if not version.startswith("v"):
119 version = f"v{version}"
120 url = f"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{self.detect_platform_slug()}"
121 else:
122 url = f"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-{self.detect_platform_slug()}"
123
124 with requests.get(url, stream=True) as response:
125 response.raise_for_status()
126 with open(self.standalone_path, "wb") as f:
127 for chunk in response.iter_content(chunk_size=8192):
128 f.write(chunk)
129
130 os.chmod(self.standalone_path, 0o755)
131
132 if not version:
133 # Get the version from the redirect chain (latest -> vX.Y.Z)
134 version = response.history[1].url.split("/")[-2]
135
136 version = version.lstrip("v")
137
138 with open(self.version_lockfile_path, "w") as f:
139 f.write(version)
140
141 return version
142
143 def install(self, version="") -> str:
144 installed_version = self.download(version)
145 self.set_version_in_config(installed_version)
146 return installed_version
147
148 @staticmethod
149 def detect_platform_slug() -> str:
150 uname = platform.uname()[0]
151
152 if uname == "Windows":
153 return "windows-x64.exe"
154
155 if uname == "Linux" and platform.uname()[4] == "aarch64":
156 return "linux-arm64"
157
158 if uname == "Linux":
159 return "linux-x64"
160
161 if uname == "Darwin" and platform.uname().machine == "arm64":
162 return "macos-arm64"
163
164 if uname == "Darwin":
165 return "macos-x64"
166
167 raise Exception("Unsupported platform for Tailwind standalone")