git: 14ea71416116 - main - www/tailwindcss4: Improve loader & inject default NODE_PATH
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 15 Sep 2026 12:40:38 UTC
The branch main has been updated by dch:
URL: https://cgit.FreeBSD.org/ports/commit/?id=14ea714161164e7af900b5619579b58c211a4d5c
commit 14ea714161164e7af900b5619579b58c211a4d5c
Author: Dave Cottlehuber <dch@FreeBSD.org>
AuthorDate: 2026-09-15 10:28:09 +0000
Commit: Dave Cottlehuber <dch@FreeBSD.org>
CommitDate: 2026-09-15 12:37:24 +0000
www/tailwindcss4: Improve loader & inject default NODE_PATH
Double-bump PORTREVISION because a private one is in use in some places.
Update a bunch of flags that broke in the latest npm update.
This simplifies most use cases to just modify the tailwind calling path,
and as a bonus it makes the testing simpler too. Most of the tests are
failure cases identified during using tailwind against
https://phoenixframework.org/ testing.
Sponsored by: SkunkWerks, GmbH
Event: EuroBSDcon 2026
---
www/tailwindcss4/Makefile | 96 +++++++++++++++++++++++++++++++++++------------
1 file changed, 71 insertions(+), 25 deletions(-)
diff --git a/www/tailwindcss4/Makefile b/www/tailwindcss4/Makefile
index 700fa3b58a1c..ab2b7376fa87 100644
--- a/www/tailwindcss4/Makefile
+++ b/www/tailwindcss4/Makefile
@@ -1,6 +1,6 @@
PORTNAME= tailwindcss4
DISTVERSION= 4.3.3
-PORTREVISION= 1
+PORTREVISION= 3
CATEGORIES= www devel
MASTER_SITES= LOCAL/dch/${PORTNAME}/:npm
MASTER_SITES+= https://codeload.github.com/tailwindlabs/tailwindcss/tar.gz/v${DISTVERSION}?dummy=/:twsrc \
@@ -63,8 +63,7 @@ do-extract:
do-build:
# because nodejs is fiddly we can't build & then install
- ${ECHO_CMD} build_from_source=true > ${WRKDIR}/.npmrc
- ${ECHO_CMD} offline=true >> ${WRKDIR}/.npmrc
+ ${ECHO_CMD} offline=true > ${WRKDIR}/.npmrc
# npm's self-update check ignores offline=true; keep the build off the network
${ECHO_CMD} update-notifier=false >> ${WRKDIR}/.npmrc
${ECHO_CMD} python=${PYTHON_CMD} >> ${WRKDIR}/.npmrc
@@ -86,14 +85,20 @@ do-build:
do-install:
# scrub MAKEFLAGS/MFLAGS as it breaks node-gyp & gmake
+# npm 12 breaks a lot of things
+# - blocks dependency install scripts which skips .node builds
+# - rejects --build-from-source as an unknown flag
+# - @parcel/watcher reads npm_config_build_from_source from env
(cd ${WRKDIR} && \
${SETENV} -u MAKEFLAGS -u MFLAGS \
HOME=${WRKDIR} npm_config_cache=${_NPM_CACHE} \
+ npm_config_build_from_source=true \
npm_config_devdir=${_NPM_CACHE} npm_config_nodedir=${PREFIX} \
npm_config_node_gyp_include_dir=${PREFIX}/include/node \
- npm install --global --no-audit --no-fund --no-prune --cache \
+ npm install --global --no-audit --no-fund --cache \
${_NPM_CACHE} --prefer-offline --verbose --no-progress \
- --build-from-source --prefix=${STAGEDIR}${PREFIX} @parcel/watcher@${_WATCHER_VER} \
+ --allow-scripts=@parcel/watcher \
+ --prefix=${STAGEDIR}${PREFIX} @parcel/watcher@${_WATCHER_VER} \
@tailwindcss/cli@${_TAILWIND_VER})
# create symlinks for the CLI tools
${LN} -sf ../lib/node_modules/@tailwindcss/cli/dist/index.mjs \
@@ -129,7 +134,7 @@ do-install:
${_WATCHER_PKG}/node_modules/@parcel/watcher-freebsd-arm64/
${PRINTF} '{"name":"@parcel/watcher-freebsd-arm64","version":"%s","main":"watcher.node"}\n' \
${_WATCHER_VER} > ${_WATCHER_PKG}/node_modules/@parcel/watcher-freebsd-arm64/package.json
-# the x86 shims we replaced shipped a LICENSE; keep one alongside ours. oxide &
+# the x86 shims we replaced shipped a LICENSE, keep one alongside ours. oxide &
# lightningcss come from the source trees we built, watcher from its own package
${INSTALL_DATA} ${_TAILWIND_SRC}/LICENSE \
${_TW_MODULES}/@tailwindcss/oxide-freebsd-arm64/
@@ -138,11 +143,41 @@ do-install:
${INSTALL_DATA} ${_WATCHER_PKG}/LICENSE \
${_WATCHER_PKG}/node_modules/@parcel/watcher-freebsd-arm64/
.endif
-# upstream jS plugin resolvers probe only .js/.json/.node/.ts
-# Phoenix & DaisyUI reference plugins that way, so append .mjs/.cjs to the resolver
+# upstream JS plugin resolvers probe only .js/.json/.node/.ts
+# Phoenix & DaisyUI reference plugins by appending .mjs/.cjs to the resolver
@${REINPLACE_CMD} -e 's|extensions:\["\.js","\.json","\.node","\.ts"\]|extensions:[".js",".json",".node",".ts",".mjs",".cjs"]|g' \
${_TW_NODE_DIST}/index.js ${_TW_NODE_DIST}/index.mjs
+# `@import "tailwindcss"`, and the plugin imports inside jiti-loaded user
+# configs, are resolved from the user's own file, so our globally installed
+# package is unreachable by that path. prepend both node_modules dirs to
+# NODE_PATH from inside the resolver module itself: enhanced-resolve folds
+# NODE_PATH into its module list, and _initPaths() makes the CJS loader that
+# jiti uses pick it up too. the paths are relative to this file, so they work
+# in STAGEDIR while testing and under PREFIX once installed
+ @${REINPLACE_CMD} -e 's|"use strict";|&{const d=__dirname+"/../../..",g=__dirname+"/../../../../../..";process.env.NODE_PATH=d+":"+g+(process.env.NODE_PATH?":"+process.env.NODE_PATH:"");require("module")._initPaths();}|' \
+ ${_TW_NODE_DIST}/index.js
+ @${REINPLACE_CMD} -e 's|import\*as M from"module";|&{const d=import.meta.dirname+"/../../..",g=import.meta.dirname+"/../../../../../..";process.env.NODE_PATH=d+":"+g+(process.env.NODE_PATH?":"+process.env.NODE_PATH:"");M.Module._initPaths();}|' \
+ ${_TW_NODE_DIST}/index.mjs
+# a plugin bundled as CJS by esbuild exports {__esModule:true,default:fn}, so
+# upstream's `m.default ?? m` gives tailwind the wrapper instead of the plugin
+# and it reports `does not accept options`. daisyUI ships such a bundle, both
+# in deps/daisyui/packages/bundle and Elixir's phx.new vendored version. Unwrap
+# the extra default the way an ESM importer would.
+ @${REINPLACE_CMD} -e 's|module:\([A-Za-z_$$][A-Za-z0-9_$$]*\)\.default??\1|module:(m=>m\&\&m.__esModule\&\&"default" in m?m.default:m)(\1.default??\1)|g' \
+ ${_TW_NODE_DIST}/index.js ${_TW_NODE_DIST}/index.mjs
@${RM} -f ${_TW_NODE_DIST}/index.js.bak ${_TW_NODE_DIST}/index.mjs.bak
+ @interop=$$(${AWK} '{ n += gsub(/"default" in m\?m\.default:m/, "&") } END { print n + 0 }' \
+ ${_TW_NODE_DIST}/index.js ${_TW_NODE_DIST}/index.mjs); \
+ if [ "$$interop" != "4" ]; then \
+ ${ECHO_CMD} "===> CJS default unwrap applied $$interop of 4 times; upstream layout changed"; \
+ exit 1; \
+ fi
+ @nodepath=$$(${GREP} -c '_initPaths' ${_TW_NODE_DIST}/index.js \
+ ${_TW_NODE_DIST}/index.mjs | ${AWK} -F: '{ n += $$2 } END { print n }'); \
+ if [ "$$nodepath" != "2" ]; then \
+ ${ECHO_CMD} "===> NODE_PATH prologue applied $$nodepath of 2 times; upstream layout changed"; \
+ exit 1; \
+ fi
# fail loudly rather than silently shipping a CLI that cannot load .mjs plugins
# the files are minified onto one line, so count occurrences, not lines
@patched=$$(${AWK} '{ n += gsub(/"\.mjs","\.cjs"\]/, "&") } END { print n + 0 }' \
@@ -157,55 +192,66 @@ do-install:
# Basic functional tests:
# oxide scanner, variant engine, lightningcss minification, TypeScript
-# plugin loading (jiti + babel + the shipped tailwindcss JS API), & Phoenix
+# plugin loading (jiti + babel + the shipped tailwindcss JS API), & Phoenix.
+# the two package-resolved plugins are withOptions plugins taking an options
+# block, as daisyUI is used from a real app.css, and they require/import
+# tailwindcss/plugin from where they live rather than from the CLI
_TEST_DIR= ${WRKDIR}/twtest
-# User plugins are resolved from where they live, not from the CLI, so the
-# installed tailwindcss package has to be reachable for "tailwindcss/plugin"
-_TEST_NODE_MODULES= ${STAGEDIR}${PREFIX}/lib/node_modules/@tailwindcss/cli/node_modules
do-test:
${RM} -r ${_TEST_DIR}
${MKDIR} ${_TEST_DIR} ${_TEST_DIR}/deps/daisyui/packages/bundle \
${_TEST_DIR}/vendor
- ${PRINTF} '@import "tailwindcss";\n@plugin "./plug.ts";\n@plugin "daisyui/packages/bundle/daisyui";\n@plugin "daisyui/packages/bundle/daisyui-theme";\n@plugin "./vendor/fake";\n' \
+ ${PRINTF} '@import "tailwindcss";\n@plugin "./plug.ts";\n@plugin "daisyui/packages/bundle/daisyui" {\n zoom: 3;\n}\n@plugin "daisyui/packages/bundle/daisyui-theme" {\n zoom: 4;\n}\n@plugin "./vendor/fake";\n@plugin "./vendor/bundle.js" {\n zoom: 6;\n}\n' \
> ${_TEST_DIR}/input.css
${PRINTF} 'import plugin from "tailwindcss/plugin";\nexport default plugin(({ addUtilities }: any) => addUtilities({ ".zz": { zoom: "2" } }));\n' \
> ${_TEST_DIR}/plug.ts
- ${PRINTF} 'module.exports = { handler: ({ addUtilities }) => addUtilities({ ".phx-cjs": { zoom: "3" } }) };\n' \
+ ${PRINTF} 'const plugin = require("tailwindcss/plugin");\nmodule.exports = plugin.withOptions((o = {}) => ({ addUtilities }) => addUtilities({ ".phx-cjs": { zoom: String(o.zoom) } }));\n' \
> ${_TEST_DIR}/deps/daisyui/packages/bundle/daisyui.js
- ${PRINTF} 'export default { handler: ({ addUtilities }) => addUtilities({ ".phx-esm": { zoom: "4" } }) };\n' \
+ ${PRINTF} 'import plugin from "tailwindcss/plugin";\nexport default plugin.withOptions((o = {}) => ({ addUtilities }) => addUtilities({ ".phx-esm": { zoom: String(o.zoom) } }));\n' \
> ${_TEST_DIR}/deps/daisyui/packages/bundle/daisyui-theme.mjs
${PRINTF} 'export default { handler: ({ addUtilities }) => addUtilities({ ".phx-vnd": { zoom: "5" } }) };\n' \
> ${_TEST_DIR}/vendor/fake.mjs
- ${PRINTF} '<div class="flex zz underline md:grid-cols-2 hover:bg-red-500/50 phx-cjs phx-esm phx-vnd">x</div>\n' \
+# esbuild emits CJS bundles as {__esModule:true,default:...}; daisyUI ships one
+ ${PRINTF} 'const plugin = require("tailwindcss/plugin");\nmodule.exports = { __esModule: true, default: plugin.withOptions((o = {}) => ({ addUtilities }) => addUtilities({ ".phx-cjsb": { zoom: String(o.zoom) } })) };\n' \
+ > ${_TEST_DIR}/vendor/bundle.js
+ ${PRINTF} '<div class="flex zz underline md:grid-cols-2 hover:bg-red-500/50 phx-cjs phx-esm phx-vnd phx-cjsb">x</div>\n' \
> ${_TEST_DIR}/index.html
+ ${PRINTF} '@import "tailwindcss";\n' > ${_TEST_DIR}/bare.css
# minified output
(cd ${_TEST_DIR} && ${SETENV} \
- NODE_PATH=${_TEST_NODE_MODULES}:${_TEST_DIR}/deps \
+ NODE_PATH=${_TEST_DIR}/deps \
${STAGEDIR}${PREFIX}/bin/tailwindcss -i input.css -o out.css --minify)
${GREP} -q 'zoom:2' ${_TEST_DIR}/out.css
${GREP} -q 'zoom:3' ${_TEST_DIR}/out.css
${GREP} -q 'zoom:4' ${_TEST_DIR}/out.css
${GREP} -q 'zoom:5' ${_TEST_DIR}/out.css
+ ${GREP} -q 'zoom:6' ${_TEST_DIR}/out.css
${GREP} -q 'grid-cols-2' ${_TEST_DIR}/out.css
${GREP} -q 'bg-red-500' ${_TEST_DIR}/out.css
# same again, unminified
(cd ${_TEST_DIR} && ${SETENV} \
- NODE_PATH=${_TEST_NODE_MODULES}:${_TEST_DIR}/deps \
+ NODE_PATH=${_TEST_DIR}/deps \
${STAGEDIR}${PREFIX}/bin/tailwindcss -i input.css -o plain.css)
${GREP} -q 'zoom: 2' ${_TEST_DIR}/plain.css
# canonicalize round-trips without touching the filesystem
- ${SETENV} NODE_PATH=${_TEST_NODE_MODULES} \
- ${STAGEDIR}${PREFIX}/bin/tailwind canonicalize flex | ${GREP} -qx flex
+ ${STAGEDIR}${PREFIX}/bin/tailwind canonicalize flex | ${GREP} -qx flex
+# test @import "tailwindcss" from a tree with no node_modules, and no NODE_PATH
+ (cd ${_TEST_DIR} && ${SETENV} -u NODE_PATH \
+ ${STAGEDIR}${PREFIX}/bin/tailwindcss -i bare.css -o bare.out.css)
+ ${GREP} -q 'grid-cols-2' ${_TEST_DIR}/bare.out.css
+# -webkit-text-size-adjust only appears if the base layer of the resolved
+# tailwindcss/index.css was emitted, ie the @import really was satisfied
+ ${GREP} -q 'text-size-adjust' ${_TEST_DIR}/bare.out.css
@${ECHO_MSG} "===> tailwindcss functional tests passed"
-# Maintainer flow, architecture independent.
-# Bump _CACHE_TAG first: archives are rebuilt & must not reuse a published name
+# maintainer flow is now architecture independent
+# bump _CACHE_TAG first: archives are rebuilt & must not reuse a published name
# make _tarball # rebuild both LOCAL distfiles & distinfo
# make _upload # push them to freefall
-# Merge crates from tailwindcss & lightningcss repos before packaging.
+# merge crates from tailwindcss & lightningcss repos before packaging
_SRC_DISTFILES= tailwindcss-v${DISTVERSION}.tar.gz:twsrc \
lightningcss-v${_LCSS_VER}.tar.gz:lcsssrc
@@ -224,11 +270,11 @@ _tarball: build-depends
${SETENV} HOME=${WRKDIR} npm_config_cache=${_NPM_CACHE} \
npm_config_devdir=${_NPM_CACHE} npm_config_nodedir=${PREFIX} \
npm_config_node_gyp_include_dir=${PREFIX}/include/node \
- npm install --global --no-audit --no-fund --no-prune --cache \
+ npm install --global --no-audit --no-fund --cache \
${_NPM_CACHE} --verbose --no-progress --prefix=${WRKDIR}/stage \
@parcel/watcher@${_WATCHER_VER} @tailwindcss/cli@${_TAILWIND_VER})
-# Normally npm only caches tarballs for the host architecture.
+# normally npm only caches tarballs for the host architecture
(cd ${WRKDIR} && \
${SETENV} HOME=${WRKDIR} npm cache add --cache ${_NPM_CACHE} \
@tailwindcss/oxide-freebsd-x64@${_TAILWIND_VER} lightningcss-freebsd-x64@${_LCSS_VER} \