diff --git a/.changeset/bright-planets-smell.md b/.changeset/bright-planets-smell.md new file mode 100644 index 00000000..caeddac6 --- /dev/null +++ b/.changeset/bright-planets-smell.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-tailwind': minor +--- + +feat: add Root Tailwind plugin package with default stylesheet auto-import diff --git a/.changeset/config.json b/.changeset/config.json index 0f1d079e..536144e2 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,15 +2,21 @@ "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, - "fixed": [[ - "@blinkk/create-root", - "@blinkk/root", - "@blinkk/root-cms", - "@blinkk/root-password-protect" - ]], + "fixed": [ + [ + "@blinkk/create-root", + "@blinkk/root", + "@blinkk/root-cms", + "@blinkk/root-password-protect", + "@blinkk/root-tailwind" + ] + ], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@examples/*", "@private/*"] + "ignore": [ + "@examples/*", + "@private/*" + ] } diff --git a/packages/root-tailwind/CHANGELOG.md b/packages/root-tailwind/CHANGELOG.md new file mode 100644 index 00000000..50ed0167 --- /dev/null +++ b/packages/root-tailwind/CHANGELOG.md @@ -0,0 +1,5 @@ +# @blinkk/root-tailwind + +## 0.0.0 + +- Initial package scaffold. diff --git a/packages/root-tailwind/README.md b/packages/root-tailwind/README.md new file mode 100644 index 00000000..697b8856 --- /dev/null +++ b/packages/root-tailwind/README.md @@ -0,0 +1,61 @@ +# @blinkk/root-tailwind + +Tailwind integration plugin for Root.js. + +## Install + +```bash +pnpm add @blinkk/root-tailwind tailwindcss postcss +``` + +## Usage (`root.config.ts`) + +```ts +import {defineConfig} from '@blinkk/root'; +import {rootTailwind} from '@blinkk/root-tailwind'; + +export default defineConfig({ + plugins: [rootTailwind()], +}); +``` + +The plugin expects a project stylesheet at `styles/index.css` and injects an +import into route modules so Root includes generated CSS through its existing +asset dependency auto-injection flow. + +Create `styles/index.css` with Tailwind directives: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +## Plugin options + +```ts +rootTailwind({ + stylesheetEntry: 'styles/index.css', + content: [ + './routes/**/*.{ts,tsx,js,jsx,md,mdx}', + './layouts/**/*.{ts,tsx,js,jsx,md,mdx}', + './components/**/*.{ts,tsx,js,jsx,md,mdx}', + './templates/**/*.{ts,tsx,js,jsx,md,mdx}', + ], +}); +``` + +- `stylesheetEntry`: Relative path to the CSS entry Root should auto-include. +- `content`: Tailwind content globs used by the injected PostCSS Tailwind + plugin. Defaults are tuned for common Root directories. + +## Migration from manual `` + +If you were manually adding a stylesheet link in ``, remove it and let +Root auto-inject styles from module dependencies instead: + +1. Add `rootTailwind()` to `root.config.ts`. +2. Create `styles/index.css` with Tailwind directives. +3. Remove manual stylesheet `` tags that pointed at your compiled CSS. + +This keeps CSS loading aligned with Root’s SSR/dev dependency graph. diff --git a/packages/root-tailwind/package.json b/packages/root-tailwind/package.json new file mode 100644 index 00000000..688614f8 --- /dev/null +++ b/packages/root-tailwind/package.json @@ -0,0 +1,42 @@ +{ + "name": "@blinkk/root-tailwind", + "version": "0.0.0", + "author": "s@blinkk.com", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/blinkk/rootjs.git", + "directory": "packages/root-tailwind" + }, + "files": [ + "dist/*" + ], + "type": "module", + "module": "./dist/core.js", + "types": "./dist/core.d.ts", + "exports": { + ".": { + "types": "./dist/core.d.ts", + "import": "./dist/core.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsup-node", + "dev": "pnpm build --watch", + "test": "pnpm build" + }, + "peerDependencies": { + "@blinkk/root": "2.5.3", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0" + }, + "devDependencies": { + "@blinkk/root": "workspace:*", + "@types/node": "24.3.1", + "tsup": "8.5.0", + "typescript": "5.9.2" + } +} diff --git a/packages/root-tailwind/src/core.ts b/packages/root-tailwind/src/core.ts new file mode 100644 index 00000000..c0e1acf7 --- /dev/null +++ b/packages/root-tailwind/src/core.ts @@ -0,0 +1,109 @@ +import {Plugin} from '@blinkk/root'; +import path from 'node:path'; +import {createRequire} from 'node:module'; +import {PluginOption} from 'vite'; + +const require = createRequire(import.meta.url); + +const DEFAULT_STYLESHEET_CONTENT = `@tailwind base; +@tailwind components; +@tailwind utilities; +`; + +const DEFAULT_CONTENT_GLOBS = [ + './routes/**/*.{ts,tsx,js,jsx,md,mdx}', + './layouts/**/*.{ts,tsx,js,jsx,md,mdx}', + './components/**/*.{ts,tsx,js,jsx,md,mdx}', + './templates/**/*.{ts,tsx,js,jsx,md,mdx}', + './elements/**/*.{ts,tsx,js,jsx,md,mdx}', +]; + +export interface RootTailwindOptions { + /** + * Path to the stylesheet entry file relative to the project root. + */ + stylesheetEntry?: string; + + /** + * Tailwind content globs. If omitted, conventions for Root projects are used. + */ + content?: string[]; +} + +/** + * Root.js plugin that wires Tailwind into Vite and auto-imports a default + * stylesheet entry into route modules so Root can auto-inject generated CSS. + */ +export function rootTailwind(options: RootTailwindOptions = {}): Plugin { + const stylesheetEntry = options.stylesheetEntry || 'styles/index.css'; + const entryImportPath = toImportPath(stylesheetEntry); + const contentGlobs = options.content || DEFAULT_CONTENT_GLOBS; + + return { + name: 'root-tailwind', + vitePlugins: [ + createTailwindPostcssPlugin(contentGlobs), + createStylesheetAutoImportPlugin(entryImportPath), + ], + }; +} + +/** + * Normalizes a project-relative entry path for use in JS import statements. + */ +function toImportPath(filepath: string): string { + const normalized = filepath.split(path.sep).join('/').replace(/^\/+/, ''); + return `/${normalized}`; +} + +/** + * Loads the Tailwind PostCSS plugin and appends it to Vite CSS processing. + */ +function createTailwindPostcssPlugin(contentGlobs: string[]): PluginOption { + return { + name: 'root-tailwind-postcss', + config() { + const tailwindcss = require('tailwindcss'); + const tailwindPlugin = tailwindcss({ + content: contentGlobs, + }); + + return { + css: { + postcss: { + plugins: [tailwindPlugin], + }, + }, + }; + }, + }; +} + +/** + * Injects the default stylesheet import into route modules. + */ +function createStylesheetAutoImportPlugin( + entryImportPath: string +): PluginOption { + const routeFileRegex = /\/routes\/.*\.(tsx|jsx)$/; + return { + name: 'root-tailwind-route-stylesheet', + enforce: 'pre', + transform(code, id) { + const [idWithoutQuery] = id.split('?', 1); + const normalizedId = idWithoutQuery.split(path.sep).join('/'); + if (!routeFileRegex.test(normalizedId)) { + return null; + } + + const importLine = `import '${entryImportPath}';`; + if (code.includes(importLine)) { + return null; + } + + return `${importLine}\n${code}`; + }, + }; +} + +export {DEFAULT_CONTENT_GLOBS, DEFAULT_STYLESHEET_CONTENT}; diff --git a/packages/root-tailwind/tsconfig.json b/packages/root-tailwind/tsconfig.json new file mode 100644 index 00000000..46430771 --- /dev/null +++ b/packages/root-tailwind/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["esnext"], + "module": "node16", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "pretty": true, + "sourceMap": true, + "strict": true, + "target": "esnext", + "outDir": "dist", + "types": ["node"], + "useUnknownInCatchVariables": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "moduleResolution": "node16" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/packages/root-tailwind/tsup.config.ts b/packages/root-tailwind/tsup.config.ts new file mode 100644 index 00000000..c6328abd --- /dev/null +++ b/packages/root-tailwind/tsup.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable node/no-unpublished-import */ + +import {defineConfig} from 'tsup'; + +export default defineConfig({ + entry: { + core: './src/core.ts', + }, + sourcemap: 'inline', + target: 'node18', + dts: true, + format: ['esm'], + splitting: false, + platform: 'node', + esbuildOptions(options) { + options.tsconfig = './tsconfig.json'; + }, +});