From 3bc397b8f5dd242bf8f93f247354b6b387b4f6fa Mon Sep 17 00:00:00 2001 From: Maciej Katafiasz Date: Wed, 31 Jan 2024 17:48:37 -0800 Subject: [PATCH 1/4] Support dynamic (Jinja-generated) classes Run tailwindcss on the build dir, rather than input templates, so that dynamically-generated values are properly seen by tailwind. --- lektor_tailwind.py | 67 ++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/lektor_tailwind.py b/lektor_tailwind.py index d4e839f..d619b99 100644 --- a/lektor_tailwind.py +++ b/lektor_tailwind.py @@ -22,6 +22,7 @@ def __init__(self, env, id): self.css_path = config.get("css_path", "static/style.css") self.input_css = os.path.join(self.env.root_path, "assets", self.css_path) self.tailwind: subprocess.Popen | None = None + self.config_file = "tailwind.config.js" def on_setup_env(self, **extra): self.init_tailwindcss() @@ -29,8 +30,7 @@ def on_setup_env(self, **extra): def init_tailwindcss(self): if not os.path.exists(self.tailwind_bin): install(bin_path=self.tailwind_bin) - filename = "tailwind.config.js" - if not os.path.exists(os.path.join(self.env.root_path, filename)): + if not os.path.exists(os.path.join(self.env.root_path, self.config_file)): subprocess.run( [self.tailwind_bin, "init"], check=True, cwd=self.env.root_path ) @@ -38,35 +38,30 @@ def init_tailwindcss(self): def _run_watcher(self, output_path: str): if not self.input_exists(): return + self.tailwind = subprocess.Popen( + self._get_tailwind_args(output_path, "-w", + *(["--minify"] if os.environ.get("NODE_ENV") == "production" else [])), + cwd=output_path, + ) - cmd = [ - self.tailwind_bin, - "--input", - self.input_css, - "--output", - os.path.join(output_path, self.css_path), - "--watch", - ] - if os.environ.get("NODE_ENV") == "production": - cmd.append("--minify") - - self.tailwind = subprocess.Popen(cmd, cwd=self.env.root_path) + def _get_tailwind_args(self, output_path, *extra_args): + return [self.tailwind_bin, + "-c", + os.path.join(self.env.root_path, self.config_file), + "-i", + self.input_css, + "-o", + os.path.join(output_path, self.css_path), + *extra_args,] def input_exists(self) -> bool: return os.path.exists(self.input_css) def compile_css(self, output_path: str): subprocess.run( - [ - self.tailwind_bin, - "-i", - self.input_css, - "-o", - os.path.join(output_path, self.css_path), - "--minify", - ], + self._get_tailwind_args(output_path, "--minify"), check=True, - cwd=self.env.root_path, + cwd=output_path, ) def on_server_spawn(self, **extra): @@ -82,26 +77,10 @@ def on_server_stop(self, **extra): self.tailwind.kill() self.tailwind = None - def on_before_build_all(self, builder, **extra): - if not self.input_exists() or self.tailwind is not None or not self.watch: - return - self._run_watcher(builder.destination_path) - - def on_before_build(self, builder, source, prog, **extra): - if source.source_filename != self.input_css: + def on_after_build_all(self, builder, **extra): + if not self.input_exists() or self.tailwind is not None: return - - # The input stylesheet is being built. We don't want to let - # Lektor "build" it (i.e. copy it to the output directory), - # since that will potentially overwrite any Tailwind-compiled - # output that is already there. - - # Here we monkey-patch Lektor's build program to disable it - prog.build_artifact = lambda artifact: None - - # Instead, we run tailwind to compile the self.input_css to - # the output directory. (We skip this if we're already - # running tailwind in --watch mode, since, in that case, it - # will rebuild the CSS on it's own.) - if self.tailwind is None: + if self.watch: + self._run_watcher(builder.destination_path) + else: self.compile_css(builder.destination_path) From df89f8a2f6e2620c84d46bd8b143ceb7fa648123 Mon Sep 17 00:00:00 2001 From: Maciej Katafiasz Date: Fri, 1 Mar 2024 08:32:55 -0800 Subject: [PATCH 2/4] Don't use Tailwind watcher Running Tailwind watcher from within Lektor watcher is unnecessary, since we already have a watcher and know when to rebuild things, and it leads to bugs, such as the Tailwind watcher leaking memory or kicking in before the build is fully done. --- lektor_tailwind.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/lektor_tailwind.py b/lektor_tailwind.py index d619b99..75e7150 100644 --- a/lektor_tailwind.py +++ b/lektor_tailwind.py @@ -6,9 +6,7 @@ from lektor.pluginsystem import Plugin from pytailwindcss import get_bin_path, install -__version__ = "0.1.2" -GRACEFUL_TIMEOUT = 5 - +__version__ = "0.1.3" class TailwindPlugin(Plugin): name = "lektor-tailwind" @@ -35,15 +33,6 @@ def init_tailwindcss(self): [self.tailwind_bin, "init"], check=True, cwd=self.env.root_path ) - def _run_watcher(self, output_path: str): - if not self.input_exists(): - return - self.tailwind = subprocess.Popen( - self._get_tailwind_args(output_path, "-w", - *(["--minify"] if os.environ.get("NODE_ENV") == "production" else [])), - cwd=output_path, - ) - def _get_tailwind_args(self, output_path, *extra_args): return [self.tailwind_bin, "-c", @@ -58,8 +47,9 @@ def input_exists(self) -> bool: return os.path.exists(self.input_css) def compile_css(self, output_path: str): + minify = [] if self.watch else ["--minify"] subprocess.run( - self._get_tailwind_args(output_path, "--minify"), + self._get_tailwind_args(output_path, *minify), check=True, cwd=output_path, ) @@ -67,20 +57,7 @@ def compile_css(self, output_path: str): def on_server_spawn(self, **extra): self.watch = True - def on_server_stop(self, **extra): - self.watch = False - if self.tailwind is not None: - self.tailwind.terminate() - try: - self.tailwind.communicate(GRACEFUL_TIMEOUT) - except subprocess.TimeoutExpired: - self.tailwind.kill() - self.tailwind = None - def on_after_build_all(self, builder, **extra): - if not self.input_exists() or self.tailwind is not None: + if not self.input_exists(): return - if self.watch: - self._run_watcher(builder.destination_path) - else: - self.compile_css(builder.destination_path) + self.compile_css(builder.destination_path) From ecb282a2f949c44ff6028364858fd29773f755e1 Mon Sep 17 00:00:00 2001 From: Maciej Katafiasz Date: Wed, 6 Mar 2024 20:17:05 -0800 Subject: [PATCH 3/4] Fix tests and README --- README.md | 3 ++- lektor_tailwind.py | 5 ++++- tests/example/tailwind.config.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f640f13..b361b3f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ A Lektor plugin that adds Tailwind CSS to your project seamlessly. ```javascript module.exports = { - content: ['./templates/**/*.{html,j2}'], + // './' refers to the lektor build output directory, NOT the project dir + content: ['./**/*.html'], theme: { extend: {}, }, diff --git a/lektor_tailwind.py b/lektor_tailwind.py index 75e7150..08e8bf1 100644 --- a/lektor_tailwind.py +++ b/lektor_tailwind.py @@ -46,8 +46,11 @@ def _get_tailwind_args(self, output_path, *extra_args): def input_exists(self) -> bool: return os.path.exists(self.input_css) + def should_minify(self) -> bool: + return not self.watch or os.environ.get("NODE_ENV") == "production" + def compile_css(self, output_path: str): - minify = [] if self.watch else ["--minify"] + minify = ["--minify"] if self.should_minify() else [] subprocess.run( self._get_tailwind_args(output_path, *minify), check=True, diff --git a/tests/example/tailwind.config.js b/tests/example/tailwind.config.js index 9b99583..c777e5d 100644 --- a/tests/example/tailwind.config.js +++ b/tests/example/tailwind.config.js @@ -1,7 +1,7 @@ const colors = require("tailwindcss/colors"); module.exports = { - content: ["./templates/**/*.html"], + content: ["./**/*.html"], theme: { extend: { colors: { From 6174e97929d7fa15f77044a50935efd6b72b42b8 Mon Sep 17 00:00:00 2001 From: Maciej Katafiasz Date: Thu, 7 Mar 2024 11:48:35 -0800 Subject: [PATCH 4/4] Fix timeouts in test_server_*_update() This was caused by the switch from having two watchers (lektor server and tailwindcss) to just the server. Because we made writes as soon as wait_for_server() returns, this would essentially catch the server in a short window before it was done returning from a build, and it wouldn't pick up the changes, so the rebuild would never happen. Previously this was not a concern, because the CSS was watched by one process, and other files by the other, and they delayed each other, so there was never a chance to write immediately after the rebuild was done. Now we sleep for a second before writing to prevent that. Note that this is purely an artefact of the testing process, and the fact we can do it within milliseconds. This is not possible for a human, so it doesn't come up in normal usage. --- tests/test_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 4498f22..acae4f9 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -85,7 +85,6 @@ class LektorServerFixture: def __init__(self, port=9527): proc = subprocess.Popen( ("lektor", "server", "-p", f"{port:d}"), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, @@ -101,9 +100,6 @@ def _close(): try: self.sel.close() proc.terminate() - proc.stdin.close() - proc.stdout.close() - proc.stderr.close() proc.wait(5) except (subprocess.TimeoutExpired, OSError): proc.kill() @@ -186,6 +182,8 @@ def test_server_rebuilds_css_when_template_updated( lektor_server.wait_for_build() assert ".flex" in output_css_path.read_text() + # Give the server a chance to pick up the changes before we start writing + time.sleep(1) with open(tmp_project_path / "templates/layout.html", "a") as fp: fp.write("""
\n""") @@ -200,6 +198,8 @@ def test_server_rebuilds_css_when_input_css_updated( lektor_server.wait_for_build() assert ".flex" in output_css_path.read_text() + # Give the server a chance to pick up the changes before we start writing + time.sleep(1) with open(tmp_project_path / "assets/static/style.css", "a") as fp: fp.write(".SENTINEL { color: red }\n")