Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Juan Di Toro | 303bdd4544 |
|
@ -1 +0,0 @@
|
||||||
Subproject commit cec13df7edcb480dfb111de3c74887f1c3ffb7e2
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,52 @@
|
||||||
|
# vite-plugin-scope-tailwind
|
||||||
|
|
||||||
|
> Encapsulate and scope your TailwindCSS styles to your library and prevent them affecting styles outside.
|
||||||
|
|
||||||
|
Love using TailwindCSS? Other people also love using TailwindCSS? Trying to mix them together? Usually this leads to problems as the tailwind classes such as `flex`, `bg-red-500` will clash and change specificity.
|
||||||
|
|
||||||
|
**Potential solutions**:
|
||||||
|
|
||||||
|
- A solution would be to [prefix your `TailwindCSS` styles in your libraries](https://stackoverflow.com/a/63770585), for example `my-lib-flex`, `my-lib-bg-red-500`, but this simply isn't good enough. The solution breaks down when there are multiple libraries using `TailwindCSS`. You would need a `prefix-` for each library. Unnecessary mental load.
|
||||||
|
|
||||||
|
- Another solution would be to [make the parent app important](https://stackoverflow.com/a/65907678). But this is an anti-pattern, and is a leaky abstraction. It is not feasible to tell all the consumers of your library to do this as a pre-requisite.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i vite-plugin-scope-tailwind -D
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`vite-plugin-scope-tailwind` to the rescue!
|
||||||
|
|
||||||
|
This plugin scopes/encapsulates/contains all the `TailwindCSS` styles of your library all in, without any extra hacking around.
|
||||||
|
|
||||||
|
Add the `scopeTailwind` plugin into the `plugins` list in your `vite.config.js`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import scopeTailwind from "vite-plugin-scope-tailwind";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...
|
||||||
|
plugins: [
|
||||||
|
...
|
||||||
|
scopeTailwind(), // or scopeTailwind({ react: true }) for a React app
|
||||||
|
...
|
||||||
|
],
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
react: boolean // If your app is a React app
|
||||||
|
ignore: RegExp | RegExp[] // If you want to exclude some classes from being scoped
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "vite-plugin-scope-tailwind",
|
||||||
|
"description": "A vite-plugin to encapsulate and scope your TailwindCSS styles to your library and prevent them affecting styles outside",
|
||||||
|
"version": "1.1.3",
|
||||||
|
"main": "./dist/cjs/index.cjs",
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.2.4",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.9",
|
||||||
|
"vite-plugin-dts": "^3.5.2",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/uniqid": "^5.3.2",
|
||||||
|
"app-root-path": "^3.1.0",
|
||||||
|
"uniqid": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const appendClassForReact = (id: string, modifiers: string[]) => (code: string) => {
|
||||||
|
let modifiedCode = code;
|
||||||
|
const classNameRegex = /className/g;
|
||||||
|
const foundClassName = modifiedCode.match(classNameRegex);
|
||||||
|
if (foundClassName) {
|
||||||
|
modifiedCode = modifiedCode.replace(/className: "/g, `className: "${id} `);
|
||||||
|
}
|
||||||
|
if (modifiers != null) {
|
||||||
|
modifiers.forEach(modifier => {
|
||||||
|
const regex = new RegExp(`className: ${modifier}\\(([^)]*)\\)`, 'g');
|
||||||
|
const replacement = `className: "${id} " + ${modifier}($1)`;
|
||||||
|
const found = modifiedCode.match(regex);
|
||||||
|
if (found) {
|
||||||
|
modifiedCode = modifiedCode.replace(regex, replacement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return modifiedCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendClass = (id: string) => (code: string) => {
|
||||||
|
const regex = /class/g;
|
||||||
|
const found = code.match(regex);
|
||||||
|
if (found) {
|
||||||
|
const c = code.replace(/class: "/g, `class: "${id} `);
|
||||||
|
return c;
|
||||||
|
} else {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
import path from "path";
|
||||||
|
import { AcceptedPlugin } from "postcss";
|
||||||
|
|
||||||
|
export const getPostCssConfig = async (): Promise<any | undefined> => {
|
||||||
|
try {
|
||||||
|
const {default: file} = await import(path.join(process.cwd(), "postcss.config.js"));
|
||||||
|
|
||||||
|
return file;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {default: file} = await import(path.join(process.cwd(), "postcss.config.cjs"));
|
||||||
|
|
||||||
|
return file;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {default: file} = await import(path.join(process.cwd(), "postcss.config.json"));
|
||||||
|
|
||||||
|
return file;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {default: file} = await import(path.join(process.cwd(), "postcss.config.ts"));
|
||||||
|
|
||||||
|
return file;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postCssPluginsToArray = (config: { plugins: AcceptedPlugin[] }): string[] => {
|
||||||
|
return Object.keys(config.plugins);
|
||||||
|
};
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Plugin } from "vite"
|
||||||
|
import path from "path"
|
||||||
|
import * as fs from "node:fs"
|
||||||
|
import { AcceptedPlugin } from "postcss"
|
||||||
|
import uniqid from "uniqid"
|
||||||
|
|
||||||
|
import { getPostCssConfig, postCssPluginsToArray } from "./get-postcss-config"
|
||||||
|
import { prefixPlugin } from "./postcss/prefix-tailwind"
|
||||||
|
import { appendClass, appendClassForReact } from "./append-classes"
|
||||||
|
|
||||||
|
const id = uniqid("d")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} options - The options for the plugin.
|
||||||
|
* @param {boolean} options.react - If true, the plugin will be configured for React.
|
||||||
|
* @param {RegExp | RegExp[]} options.ignore - Regular expressions to ignore.
|
||||||
|
* @param {string[]} options.classNameTransformers - Functions used inside `className` that return a string.
|
||||||
|
* @returns {Plugin} - The configured plugin.
|
||||||
|
*/
|
||||||
|
const plugin = ({
|
||||||
|
react = false,
|
||||||
|
classNameTransformers = [],
|
||||||
|
ignore = []
|
||||||
|
}: {
|
||||||
|
react?: boolean
|
||||||
|
ignore?: RegExp | RegExp[]
|
||||||
|
classNameTransformers?: string[]
|
||||||
|
} = {}): Plugin => ({
|
||||||
|
name: "vite-plugin-scope-tailwind",
|
||||||
|
config: async (config) => {
|
||||||
|
let currentPostCssPlugins: AcceptedPlugin[] = [] as AcceptedPlugin[]
|
||||||
|
if (
|
||||||
|
typeof config.css !== "undefined" &&
|
||||||
|
typeof config.css.postcss !== "string" &&
|
||||||
|
typeof config.css.postcss !== "undefined"
|
||||||
|
) {
|
||||||
|
currentPostCssPlugins =
|
||||||
|
config.css.postcss.plugins ?? currentPostCssPlugins
|
||||||
|
}
|
||||||
|
|
||||||
|
const postCssConfigFile = await getPostCssConfig()
|
||||||
|
|
||||||
|
return {
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
...currentPostCssPlugins,
|
||||||
|
...(
|
||||||
|
await Promise.all(
|
||||||
|
postCssPluginsToArray(postCssConfigFile).map(loadPackage)
|
||||||
|
)
|
||||||
|
).map((r) => r.default),
|
||||||
|
prefixPlugin({ prefix: `${id}.`, ignore })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transform: react ? appendClassForReact(id, classNameTransformers) : appendClass(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadPackage(pack: string) {
|
||||||
|
const packageJsonPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"node_modules",
|
||||||
|
pack,
|
||||||
|
"package.json"
|
||||||
|
)
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
|
||||||
|
const mainEntry = packageJson.main
|
||||||
|
|
||||||
|
return await import(path.join(process.cwd(), "node_modules", pack, mainEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default plugin
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { AcceptedPlugin } from "postcss";
|
||||||
|
/**
|
||||||
|
* Determine if class passes test
|
||||||
|
*
|
||||||
|
* @param {string} cssClass
|
||||||
|
* @param {RegExp | RegExp[]} test
|
||||||
|
*/
|
||||||
|
function classMatchesTest(cssClass: string, test: RegExp | RegExp[]) {
|
||||||
|
if (!test) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cssClass = cssClass.trim();
|
||||||
|
|
||||||
|
if (test instanceof RegExp) {
|
||||||
|
return test.exec(cssClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(test)) {
|
||||||
|
// Reassign arguments
|
||||||
|
return test.some((t) => {
|
||||||
|
if (t instanceof RegExp) {
|
||||||
|
return t.exec(cssClass);
|
||||||
|
} else {
|
||||||
|
return cssClass === t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cssClass === test;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prefixPlugin = ({
|
||||||
|
prefix,
|
||||||
|
ignore,
|
||||||
|
}: {
|
||||||
|
prefix: string;
|
||||||
|
ignore: RegExp | RegExp[];
|
||||||
|
}): AcceptedPlugin => {
|
||||||
|
return {
|
||||||
|
postcssPlugin: "prefix-tailwind-classes",
|
||||||
|
|
||||||
|
Root(root) {
|
||||||
|
root.walkRules((rule) => {
|
||||||
|
if (!rule.selectors) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.selectors = rule.selectors.map((selector) => {
|
||||||
|
// Is class selector
|
||||||
|
if (selector.indexOf(".") !== 0) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
var classes = selector.split(".");
|
||||||
|
|
||||||
|
return classes
|
||||||
|
.map((cssClass) => {
|
||||||
|
if (
|
||||||
|
classMatchesTest(cssClass, ignore) ||
|
||||||
|
cssClass.trim().length === 0
|
||||||
|
) {
|
||||||
|
return cssClass;
|
||||||
|
}
|
||||||
|
return prefix + cssClass;
|
||||||
|
})
|
||||||
|
.join(".");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import tsConfigPaths from "vite-tsconfig-paths";
|
||||||
|
import dts from "vite-plugin-dts";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tsConfigPaths(),
|
||||||
|
dts({
|
||||||
|
include: ["src"],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: resolve("src", "main.ts"),
|
||||||
|
name: "vite-plugin-scope-tailwind",
|
||||||
|
formats: ["es", "cjs"],
|
||||||
|
fileName: (format) => {
|
||||||
|
switch (format) {
|
||||||
|
case "es":
|
||||||
|
return `${format}/index.mjs`;
|
||||||
|
case "cjs":
|
||||||
|
return `${format}/index.cjs`;
|
||||||
|
default:
|
||||||
|
return "index.js";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["fs", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue