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