Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-planets-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@blinkk/root-tailwind': minor
---

feat: add Root Tailwind plugin package with default stylesheet auto-import
20 changes: 13 additions & 7 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/*"
]
}
5 changes: 5 additions & 0 deletions packages/root-tailwind/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @blinkk/root-tailwind

## 0.0.0

- Initial package scaffold.
61 changes: 61 additions & 0 deletions packages/root-tailwind/README.md
Original file line number Diff line number Diff line change
@@ -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 `<link rel="stylesheet">`

If you were manually adding a stylesheet link in `<Head>`, 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 `<link>` tags that pointed at your compiled CSS.

This keeps CSS loading aligned with Root’s SSR/dev dependency graph.
42 changes: 42 additions & 0 deletions packages/root-tailwind/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
109 changes: 109 additions & 0 deletions packages/root-tailwind/src/core.ts
Original file line number Diff line number Diff line change
@@ -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};
27 changes: 27 additions & 0 deletions packages/root-tailwind/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
18 changes: 18 additions & 0 deletions packages/root-tailwind/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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';
},
});
Loading