From 56bdb1fddffa798563ce22b1fbc9e9235ef88337 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 30 Mar 2023 00:34:10 -0400 Subject: [PATCH] use a local tailwindcss binary by setting TAILWINDCSS_INSTALL_DIR this will work for both the vanilla "ruby" platform as well as override the behavior of the native platform gem. --- CHANGELOG.md | 1 + README.md | 29 +++++++++- lib/tailwindcss/commands.rb | 45 +++++++++++----- test/lib/tailwindcss/commands_test.rb | 78 +++++++++++++++++++++++++-- 4 files changed, 133 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b776444..1d1ac58b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next / unreleased * Update to [Tailwind CSS v3.3.0](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.3.0) by @tysongach +* Users can use a locally-installed `tailwindcss` executable by setting a `TAILWINDCSS_INSTALL_DIR` environment variable. (#224, #226) by @flavorjones ## v2.0.25 / 2023-03-14 diff --git a/README.md b/README.md index db1a12cd..aa4aed4b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,34 @@ With Rails 7 you can generate a new application preconfigured with Tailwind by u 1. Run `./bin/bundle add tailwindcss-rails` 2. Run `./bin/rails tailwindcss:install` -This gem wraps [the standalone executable version](https://tailwindcss.com/blog/standalone-cli) of the Tailwind CSS v3 framework. These executables are platform specific, so there are actually separate underlying gems per platform, but the correct gem will automatically be picked for your platform. Supported platforms are Linux x64, macOS arm64, macOS x64, and Windows x64. (Note that due to this setup, you must install the actual gems – you can't pin your gem to the github repo.) +This gem wraps [the standalone executable version](https://tailwindcss.com/blog/standalone-cli) of the Tailwind CSS v3 framework. These executables are platform specific, so there are actually separate underlying gems per platform, but the correct gem will automatically be picked for your platform. + +Supported platforms are: + +- arm64-darwin (macos-arm64) +- x64-mingw32 (windows-x64) +- x64-mingw-ucr (windows-x64) +- x86_64-darwin (macos-x64) +- x86_64-linux (linux-x64) +- aarch64-linux (linux-arm64) +- arm-linux (linux-armv7) + + +### Using a local installation of `tailwindcss` + +If you are not able to use the vendored standalone executables (for example, if you're on an unsupported platform), you can use a local installation of the `tailwindcss` executable by setting an environment variable named `TAILWINDCSS_INSTALL_DIR` to the directory containing the executable. + +For example, if you've installed `tailwindcss` so that the executable is found at `/node_modules/bin/tailwindcss`, then you should set your environment variable like so: + +``` sh +TAILWINDCSS_INSTALL_DIR=/path/to/node_modules/bin +``` + +This also works with relative paths. If you've installed into your app's directory at `./node_modules/.bin/tailwindcss`: + +``` sh +TAILWINDCSS_INSTALL_DIR=node_modules/.bin +``` ## Developing with Tailwindcss diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index 4c91428c..448e08f9 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -2,6 +2,8 @@ module Tailwindcss module Commands + DEFAULT_DIR = File.expand_path(File.join(__dir__, "..", "..", "exe")) + # raised when the host platform is not supported by upstream tailwindcss's binary releases class UnsupportedPlatformException < StandardError end @@ -10,26 +12,41 @@ class UnsupportedPlatformException < StandardError class ExecutableNotFoundException < StandardError end + # raised when TAILWINDCSS_INSTALL_DIR does not exist + class DirectoryNotFoundException < StandardError + end + class << self def platform [:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-") end - def executable( - exe_path: File.expand_path(File.join(__dir__, "..", "..", "exe")) - ) - if Tailwindcss::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match(Gem::Platform.new(p)) } - raise UnsupportedPlatformException, <<~MESSAGE - tailwindcss-rails does not support the #{platform} platform - Please install tailwindcss following instructions at https://tailwindcss.com/docs/installation - MESSAGE - end - - exe_path = Dir.glob(File.expand_path(File.join(exe_path, "*", "tailwindcss"))).find do |f| - Gem::Platform.match(Gem::Platform.new(File.basename(File.dirname(f)))) + def executable(exe_path: DEFAULT_DIR) + tailwindcss_install_dir = ENV["TAILWINDCSS_INSTALL_DIR"] + if tailwindcss_install_dir + if File.directory?(tailwindcss_install_dir) + warn "NOTE: using TAILWINDCSS_INSTALL_DIR to find tailwindcss executable: #{tailwindcss_install_dir}" + exe_path = tailwindcss_install_dir + exe_file = File.expand_path(File.join(tailwindcss_install_dir, "tailwindcss")) + else + raise DirectoryNotFoundException, <<~MESSAGE + TAILWINDCSS_INSTALL_DIR is set to #{tailwindcss_install_dir}, but that directory does not exist. + MESSAGE + end + else + if Tailwindcss::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match(Gem::Platform.new(p)) } + raise UnsupportedPlatformException, <<~MESSAGE + tailwindcss-rails does not support the #{platform} platform + Please install tailwindcss following instructions at https://tailwindcss.com/docs/installation + MESSAGE + end + + exe_file = Dir.glob(File.expand_path(File.join(exe_path, "*", "tailwindcss"))).find do |f| + Gem::Platform.match(Gem::Platform.new(File.basename(File.dirname(f)))) + end end - if exe_path.nil? + if exe_file.nil? || !File.exist?(exe_file) raise ExecutableNotFoundException, <<~MESSAGE Cannot find the tailwindcss executable for #{platform} in #{exe_path} @@ -52,7 +69,7 @@ def executable( MESSAGE end - exe_path + exe_file end def compile_command(debug: false, **kwargs) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index ac8f9d05..6ddc6a25 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -2,11 +2,6 @@ require "minitest/mock" class Tailwindcss::CommandsTest < ActiveSupport::TestCase - test ".platform is a string containing just the cpu and os (not the version)" do - expected = "#{Gem::Platform.local.cpu}-#{Gem::Platform.local.os}" - assert_equal(expected, Tailwindcss::Commands.platform) - end - def mock_exe_directory(platform) Dir.mktmpdir do |dir| FileUtils.mkdir(File.join(dir, platform)) @@ -18,6 +13,19 @@ def mock_exe_directory(platform) end end + def mock_local_tailwindcss_install + Dir.mktmpdir do |dir| + path = File.join(dir, "tailwindcss") + FileUtils.touch(path) + yield(dir, path) + end + end + + test ".platform is a string containing just the cpu and os (not the version)" do + expected = "#{Gem::Platform.local.cpu}-#{Gem::Platform.local.os}" + assert_equal(expected, Tailwindcss::Commands.platform) + end + test ".executable returns the absolute path to the binary" do mock_exe_directory("sparc-solaris2.8") do |dir, executable| expected = File.expand_path(File.join(dir, "sparc-solaris2.8", "tailwindcss")) @@ -42,6 +50,66 @@ def mock_exe_directory(platform) end end + test ".executable returns the executable in TAILWINDCSS_INSTALL_DIR when no packaged binary exists" do + mock_local_tailwindcss_install do |local_install_dir, expected| + result = nil + begin + ENV["TAILWINDCSS_INSTALL_DIR"] = local_install_dir + assert_output(nil, /using TAILWINDCSS_INSTALL_DIR/) do + result = Tailwindcss::Commands.executable(exe_path: "/does/not/exist") + end + ensure + ENV["TAILWINDCSS_INSTALL_DIR"] = nil + end + assert_equal(expected, result) + end + end + + test ".executable returns the executable in TAILWINDCSS_INSTALL_DIR when we're not on a supported platform" do + Gem::Platform.stub(:match, false) do # nothing is supported + mock_local_tailwindcss_install do |local_install_dir, expected| + result = nil + begin + ENV["TAILWINDCSS_INSTALL_DIR"] = local_install_dir + assert_output(nil, /using TAILWINDCSS_INSTALL_DIR/) do + result = Tailwindcss::Commands.executable + end + ensure + ENV["TAILWINDCSS_INSTALL_DIR"] = nil + end + assert_equal(expected, result) + end + end + end + + test ".executable returns the executable in TAILWINDCSS_INSTALL_DIR even when a packaged binary exists" do + mock_exe_directory("sparc-solaris2.8") do |dir, _executable| + mock_local_tailwindcss_install do |local_install_dir, expected| + result = nil + begin + ENV["TAILWINDCSS_INSTALL_DIR"] = local_install_dir + assert_output(nil, /using TAILWINDCSS_INSTALL_DIR/) do + result = Tailwindcss::Commands.executable(exe_path: dir) + end + ensure + ENV["TAILWINDCSS_INSTALL_DIR"] = nil + end + assert_equal(expected, result) + end + end + end + + test ".executable raises ExecutableNotFoundException is TAILWINDCSS_INSTALL_DIR is set to a nonexistent dir" do + begin + ENV["TAILWINDCSS_INSTALL_DIR"] = "/does/not/exist" + assert_raises(Tailwindcss::Commands::DirectoryNotFoundException) do + Tailwindcss::Commands.executable + end + ensure + ENV["TAILWINDCSS_INSTALL_DIR"] = nil + end + end + test ".compile_command" do mock_exe_directory("sparc-solaris2.8") do |dir, executable| Rails.stub(:root, File) do # Rails.root won't work in this test suite