Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

bugfix: Properly display all component classes for our TW plugin's intellisense #1664

Merged
merged 5 commits into from
Jun 14, 2023
Merged
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/cyan-wombats-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@skeletonlabs/skeleton": patch
---

bugfix: Tailwind plugin intellisense now properly displays all component classes
2 changes: 1 addition & 1 deletion packages/skeleton/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ dist
coverage
.temp
src/lib/tailwind/generated
*.tgz
*.tgz
1 change: 1 addition & 0 deletions packages/skeleton/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ pnpm-lock.yaml
/.temp
/dist
/scripts/tw-settings.json
/src/lib/tailwind/generated
2 changes: 1 addition & 1 deletion packages/skeleton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"format": "prettier --ignore-path .prettierignore --write --plugin-search-dir=. .",
"test": "vitest",
"coverage": "vitest run --coverage",
"sync": "svelte-kit sync",
"sync": "svelte-kit sync && pnpm build:jss",
"build:jss": "node ./scripts/generate-jss.js --package"
},
"repository": {
Expand Down
66 changes: 38 additions & 28 deletions packages/skeleton/scripts/compile-css-to-js.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,56 @@ const postcssImport = require('postcss-import');
const tailwindcss = require('tailwindcss');

// Transpiles all of our library's CSS to JS
async function transpileCssToJs() {
const cssEntryPath = './src/lib/styles/skeleton.css';
async function transpileCssToJs(cssEntryPath, plugins = []) {
const selectors = [];

// We'll first get all the custom the class names,
// then we can feed that into the TW preprocessor as the `content`
// so that TW can detect and generate them properly

const css = fs.readFileSync(cssEntryPath, 'utf8');
const processor = postcss([postcssImport()]);
const result = await processor.process(css, { from: cssEntryPath });

result.root.walk((node) => {
if (node.type === 'rule') {
selectors.push(...node.selectors);
}
});

// Custom tailwind config so that we only use the necessities
const twConfig = {
darkMode: 'class',
content: ['./src/**/*.{html,js,svelte,ts}'],
plugins: [require('../src/lib/tailwind/core.cjs')]
content: [{ raw: selectors.join(' ') }],
plugins: [
require('../src/lib/tailwind/core.cjs'),
// add skeleton component classes for the base styles
...plugins
]
};

const css = fs.readFileSync(cssEntryPath, 'utf8');
const result = await postcss().use(postcssImport()).use(tailwindcss(twConfig)).process(css, { from: cssEntryPath });
const cssInJs = postcssJs.objectify(result.root);
const result2 = await postcss([postcssImport(), tailwindcss(twConfig)]).process(css, { from: cssEntryPath });
if (result2.root.type === 'document') throw Error('This should never happen');

return structuredClone(cssInJs); // return as a POJO
}
const cssInJs = postcssJs.objectify(result2.root);

// Generates all of the TW classes so that we can use this to remove duplicates in our plugin.
// Takes ~8 seconds to run.
async function generateAllTWClasses() {
console.log("First time running, generating all tailwind classes... this may take a while... (it's only once though!)");
const cssEntryPath = './src/lib/styles/partials/tailwind.css';
return cssInJs;
}

// Special tailwind config so that all TW classes are included
// Generates all TW base styles so that we can use this to
// the remove duplicates in our plugin.
// Takes ~600ms second to run.
async function generateBaseTWStyles() {
const twConfig = {
content: [{ raw: '' }],
safelist: [
{
pattern: /.*/
}
]
content: [{ raw: 'w-1' }]
};

const css = fs.readFileSync(cssEntryPath, 'utf8');
const result = await postcss().use(tailwindcss(twConfig)).process(css, { from: cssEntryPath });
const cssInJs = postcssJs.objectify(result.root);
const result = await postcss(tailwindcss(twConfig)).process('@tailwind base', { from: undefined });
if (result.root.type === 'document') throw Error('This should never happen');

// Caches the TW classes so we don't have to generate them again after the initial run
fs.writeFileSync('./.temp/twClasses.cjs', `module.exports = ${JSON.stringify(cssInJs)}`);
const cssInJs = postcssJs.objectify(result.root);

return structuredClone(cssInJs); // return as a POJO
return cssInJs;
}

module.exports = { transpileCssToJs, generateAllTWClasses };
module.exports = { transpileCssToJs, generateBaseTWStyles };
93 changes: 29 additions & 64 deletions packages/skeleton/scripts/generate-jss.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { generateAllTWClasses, transpileCssToJs } from './compile-css-to-js.cjs';
import { mkdir, writeFile, rename, unlink } from 'fs/promises';
import { generateBaseTWStyles, transpileCssToJs } from './compile-css-to-js.cjs';
import { mkdir, writeFile, unlink } from 'fs/promises';

const INTELLISENSE_FILE_NAME = 'intellisense-classes.cjs';

Expand All @@ -13,86 +13,51 @@ async function exec() {
// file doesn't exist, don't worry about it
});

// Makes directory that's used for caching
await mkdir('./.temp').catch(() => {
// directory already exists
});

// Makes directory that stores our generated CSS-in-JS
await mkdir('./src/lib/tailwind/generated').catch(() => {
// directory already exists
});

const generatedJSS = await transpileCssToJs();
const purgedJSS = await removeDuplicateClasses(generatedJSS);
const baseTWStyles = await generateBaseTWStyles();
const generatedComponentJSS = await transpileCssToJs('./src/lib/styles/skeleton.css');
const cleanedComponentClasses = removeDuplicateClasses(generatedComponentJSS, baseTWStyles);
const componentClasses = patchMediaQueries(cleanedComponentClasses);

// Creates the generated CSS-in-JS file
await writeFile(`./src/lib/tailwind/generated/${INTELLISENSE_FILE_NAME}`, `module.exports = ${JSON.stringify(purgedJSS)}`).catch((e) =>
console.error(e)
await writeFile(`./src/lib/tailwind/generated/${INTELLISENSE_FILE_NAME}`, `module.exports = ${JSON.stringify(componentClasses)}`).catch(
(e) => console.error(e)
);

// any extra param here indicates that we are being called by the package script, otherwise argv.length is 2 by default
if (process.argv.length != 3) {
// A roundabout 'hack' to retrigger the tailwind extension to reload,
// otherwise we'd have to reload vscode manually.
await rename('./tailwind.config.cjs', './.temp/tailwind.config.cjs');
// We need to sleep for a bit so that the change is detected
// by the extension's file watcher
await new Promise((resolve) => setTimeout(resolve, 3000));
await rename('./.temp/tailwind.config.cjs', './tailwind.config.cjs');
}
}

// Purges the generated CSS-in-JS file of duplicate TW classes
async function removeDuplicateClasses(cssInJs) {
let twClasses;
try {
// import the cached TW classes...
const classes = await import('../.temp/twClasses.cjs');
twClasses = classes.default;
} catch {
// if the cache doesn't exist (first time install), generate it
twClasses = await generateAllTWClasses();
function removeDuplicateClasses(cssInJs, baseTWStyles) {
for (const key of Object.keys(cssInJs)) {
// We'll delete all the TW Base styles (i.e. html {...} body {...} etc.)
if (baseTWStyles[key] !== undefined) delete cssInJs[key];
if (key.includes(':is')) continue;
if (key.startsWith('@media')) continue;
// deletes the dark variant of type selectors (ex: .dark body {...})
if (key.startsWith('.dark') && key[6] !== '.') delete cssInJs[key];
// if it's not a class selector, delete it (only want classes in the intellisense)
if (key[0] !== '.') delete cssInJs[key];
}

const mediaQueries = [];
return cssInJs;
}

// We delete classes that have 'token' and 'bg-' in their name since those classes
// will already be generated by our plugin. We'll also delete any default TW classes.
for (const [key] of Object.entries(cssInJs)) {
// if it's a media query, add it to the mediaQueries array to process later
// Moves all of the media queries towards the end of the cssInJs object.
function patchMediaQueries(cssInJs) {
const mediaQueries = {};

for (const key of Object.keys(cssInJs)) {
if (key.startsWith('@media')) {
const isIterable = typeof cssInJs[key][Symbol.iterator] === 'function';
if (isIterable) mediaQueries.push([key, cssInJs[key]]);
else mediaQueries.push([key, [cssInJs[key]]]);
mediaQueries[key] = cssInJs[key];
delete cssInJs[key];
}

// if it's a token, delete it
if (key.includes('token')) delete cssInJs[key];

// if it's a background color, delete it
if (key.includes('bg-')) delete cssInJs[key];

// if it's not a class selector, delete it (only want classes in the intellisense)
if (key[0] !== '.') delete cssInJs[key];
// deletes the dark variant of type selectors (ex: .dark body {...})
if (key.startsWith('.dark') && key[6] !== '.') delete cssInJs[key];

// if it's a default tailwind class, delete it
if (twClasses[key]) delete cssInJs[key];
}

// Add media queries of non-duplicate classes back to cssInJs
for (const [mediaQuery, selectorsArray] of mediaQueries) {
for (const selectors of selectorsArray) {
for (const [selector, CSS] of Object.entries(selectors)) {
if (selector in cssInJs) {
// Non-duplicate class, add media query to cssInJs
if (!cssInJs[selector][mediaQuery]) cssInJs[selector][mediaQuery] = [];
cssInJs[selector][mediaQuery].push(CSS); // Add media query to class
}
}
}
for (const key of Object.keys(mediaQueries)) {
cssInJs[key] = mediaQueries[key];
}

return cssInJs;
Expand Down