Compare commits

...

1 Commits

Author SHA1 Message Date
Juan Di Toro 303bdd4544 fix: plugin not on path 2023-10-09 14:53:36 +02:00
12 changed files with 1740 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit cec13df7edcb480dfb111de3c74887f1c3ffb7e2

View File

@ -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?

View File

@ -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

View File

@ -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"
}
}

View File

@ -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;
}
};

View File

@ -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);
};

View File

@ -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

View File

@ -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(".");
});
});
},
};
};

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -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"]
}

View File

@ -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"],
},
},
});