forgecss 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +22 -17
- package/index.d.ts +15 -13
- package/index.js +82 -43
- package/lib/generator.js +29 -17
- package/lib/inventory.d.ts +3 -0
- package/lib/inventory.js +26 -0
- package/lib/processor.d.ts +3 -0
- package/lib/{processFile.js → processor.js} +32 -27
- package/lib/transformers/mediaQuery.js +44 -0
- package/lib/transformers/pseudo.js +57 -0
- package/package.json +3 -2
- package/tsconfig.json +3 -0
- package/lib/styles.js +0 -61
package/cli.js
CHANGED
|
@@ -7,13 +7,13 @@ import { program } from "commander";
|
|
|
7
7
|
import ForgeCSS from './index.js';
|
|
8
8
|
import chokidar from "chokidar";
|
|
9
9
|
|
|
10
|
-
program.option("--config", "Path to forgecss config file", process.cwd() + "/forgecss.config.js");
|
|
11
|
-
program.option("--watch", "Enable watch mode", false);
|
|
12
|
-
program.option("--verbose", "Enable watch mode", false);
|
|
10
|
+
program.option("-c, --config <string>,", "Path to forgecss config file", process.cwd() + "/forgecss.config.js");
|
|
11
|
+
program.option("-w, --watch", "Enable watch mode", false);
|
|
12
|
+
program.option("-v, --verbose", "Enable watch mode", false);
|
|
13
13
|
program.parse();
|
|
14
14
|
|
|
15
15
|
const options = program.opts();
|
|
16
|
-
let config = null;
|
|
16
|
+
let config = null, instance = null;
|
|
17
17
|
|
|
18
18
|
if (!fs.existsSync(options.config)) {
|
|
19
19
|
throw new Error(`forgecss: Config file not found at ${options.config}. Check the --config option.`);
|
|
@@ -24,22 +24,24 @@ async function loadConfig(configPath) {
|
|
|
24
24
|
const fileUrl = pathToFileURL(abs).href;
|
|
25
25
|
|
|
26
26
|
const mod = await import(fileUrl);
|
|
27
|
-
return mod.default ?? mod;
|
|
27
|
+
return mod.default ?? mod;
|
|
28
28
|
}
|
|
29
29
|
async function runForgeCSS(lookAtPath = null) {
|
|
30
30
|
if (!config) {
|
|
31
31
|
// The very first run
|
|
32
32
|
config = await loadConfig(options.config);
|
|
33
|
+
if (!config.dir) {
|
|
34
|
+
throw new Error('forgecss: missing "dir" in configuration.');
|
|
35
|
+
}
|
|
36
|
+
if (!config.output) {
|
|
37
|
+
throw new Error('forgecss: missing "output" in configuration.');
|
|
38
|
+
}
|
|
39
|
+
instance = ForgeCSS(config);
|
|
33
40
|
if (options.watch) {
|
|
34
|
-
const watcher = chokidar.watch(config.
|
|
41
|
+
const watcher = chokidar.watch(config.dir, {
|
|
35
42
|
persistent: true,
|
|
36
43
|
ignoreInitial: true,
|
|
37
|
-
ignored: (p, stats) =>
|
|
38
|
-
if (path.resolve(p) === path.resolve(config.output)) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
44
|
+
ignored: (p, stats) => path.resolve(p) === path.resolve(config.output)
|
|
43
45
|
});
|
|
44
46
|
watcher.on("change", async (filePath) => {
|
|
45
47
|
if (options.verbose) {
|
|
@@ -52,11 +54,14 @@ async function runForgeCSS(lookAtPath = null) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
57
|
+
if (lookAtPath) {
|
|
58
|
+
instance.parseFile(lookAtPath, config.output);
|
|
59
|
+
} else {
|
|
60
|
+
instance.parseDirectory(config.dir, config.output);
|
|
61
|
+
}
|
|
62
|
+
if (options.verbose) {
|
|
63
|
+
console.log(`forgecss: ${config.output} generated successfully.`);
|
|
64
|
+
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
runForgeCSS();
|
package/index.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
export type ForgeCSSOptions = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[key: string]: {
|
|
9
|
-
query: string;
|
|
10
|
-
};
|
|
2
|
+
inventoryFiles?: string[];
|
|
3
|
+
usageFiles?: string[];
|
|
4
|
+
usageAttributes?: string[];
|
|
5
|
+
mapping?: {
|
|
6
|
+
queries?: {
|
|
7
|
+
[key: string]: string
|
|
11
8
|
};
|
|
12
9
|
};
|
|
13
|
-
output: string;
|
|
14
10
|
};
|
|
15
11
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
export type ForgeInstance = {
|
|
13
|
+
parseDirectory: (directoryPath: string, outputFile?: string) => Promise<string>;
|
|
14
|
+
parseFile: (filePath: string, outputFile?: string) => Promise<string>;
|
|
15
|
+
parse: (css: string, html: string, outputFile?: string) => Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
declare function ForgeCSS(options?: ForgeCSSOptions): ForgeInstance;
|
|
19
|
+
|
|
20
|
+
export default ForgeCSS;
|
package/index.js
CHANGED
|
@@ -1,72 +1,111 @@
|
|
|
1
|
+
import { writeFile } from "fs/promises";
|
|
1
2
|
import getAllFiles from "./lib/getAllFiles.js";
|
|
2
|
-
import { extractStyles } from "./lib/
|
|
3
|
-
import {
|
|
3
|
+
import { extractStyles, invalidateInvetory } from "./lib/inventory.js";
|
|
4
|
+
import { invalidateUsageCache, findUsages } from "./lib/processor.js";
|
|
4
5
|
import { generateOutputCSS } from "./lib/generator.js";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_OPTIONS = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
declarationsMatchAttributes: ['class', 'className'],
|
|
8
|
+
inventoryFiles: ["css", "less", "scss"],
|
|
9
|
+
usageFiles: ["html", "jsx", "tsx"],
|
|
10
|
+
usageAttributes: ["class", "className"],
|
|
11
11
|
mapping: {
|
|
12
12
|
queries: {}
|
|
13
|
-
}
|
|
14
|
-
output: null
|
|
13
|
+
}
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
export default function
|
|
16
|
+
export default function ForgeCSS(options) {
|
|
18
17
|
const config = { ...DEFAULT_OPTIONS };
|
|
19
18
|
|
|
20
|
-
config.
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
config.mapping = {
|
|
20
|
+
queries: Object.assign({}, DEFAULT_OPTIONS.mapping.queries, options?.mapping?.queries ?? {})
|
|
21
|
+
};
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
async function result(output) {
|
|
24
|
+
try {
|
|
25
|
+
const css = await generateOutputCSS(config);
|
|
26
|
+
if (output) {
|
|
27
|
+
await writeFile(output, `/* ForgeCSS autogenerated file */\n${css}`, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
return css;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`forgecss: error generating output CSS: ${err}`);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
return {
|
|
32
|
-
async
|
|
33
|
-
|
|
37
|
+
async parseDirectory(dir, output = null) {
|
|
38
|
+
if (!dir) {
|
|
39
|
+
throw new Error('forgecss: parseDirectory requires "dir" as an argument.');
|
|
40
|
+
}
|
|
34
41
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} else {
|
|
40
|
-
let files = await getAllFiles(config.source, config.stylesMatch);
|
|
41
|
-
for (let file of files) {
|
|
42
|
-
await extractStyles(file);
|
|
43
|
-
}
|
|
42
|
+
// filling the inventory
|
|
43
|
+
let files = await getAllFiles(dir, config.inventoryFiles);
|
|
44
|
+
for (let file of files) {
|
|
45
|
+
await extractStyles(file);
|
|
44
46
|
}
|
|
45
47
|
} catch (err) {
|
|
46
|
-
console.error(`forgecss: error extracting styles
|
|
48
|
+
console.error(`forgecss: error extracting styles.`, err);
|
|
47
49
|
}
|
|
48
|
-
//
|
|
50
|
+
// finding the usages
|
|
49
51
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await extractDeclarations(lookAtPath);
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
let files = await getAllFiles(config.source, config.declarationsMatch);
|
|
57
|
-
for (let file of files) {
|
|
58
|
-
await extractDeclarations(file);
|
|
59
|
-
}
|
|
52
|
+
let files = await getAllFiles(dir, config.usageFiles);
|
|
53
|
+
for (let file of files) {
|
|
54
|
+
await findUsages(file);
|
|
60
55
|
}
|
|
61
56
|
} catch (err) {
|
|
62
|
-
console.error(`forgecss: error extracting
|
|
57
|
+
console.error(`forgecss: error extracting usages`, err);
|
|
63
58
|
}
|
|
64
59
|
// generating the output CSS
|
|
60
|
+
return result(output);
|
|
61
|
+
},
|
|
62
|
+
async parseFile(file, output = null) {
|
|
63
|
+
if (!file) {
|
|
64
|
+
throw new Error('forgecss: parseFile requires "file" as an argument.');
|
|
65
|
+
}
|
|
66
|
+
const ext = file.split(".").pop().toLowerCase();
|
|
67
|
+
// filling the inventory
|
|
68
|
+
try {
|
|
69
|
+
if (config.inventoryFiles.includes(ext)) {
|
|
70
|
+
await extractStyles(file);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`forgecss: error extracting styles.`, err);
|
|
74
|
+
}
|
|
75
|
+
// finding the usages
|
|
76
|
+
try {
|
|
77
|
+
if (config.usageFiles.includes(ext)) {
|
|
78
|
+
invalidateUsageCache(file);
|
|
79
|
+
await findUsages(file);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`forgecss: error extracting usages.`, err);
|
|
83
|
+
}
|
|
84
|
+
// generating the output CSS
|
|
85
|
+
return result(output);
|
|
86
|
+
},
|
|
87
|
+
async parse(css, html, output = null) {
|
|
88
|
+
if (!css) {
|
|
89
|
+
throw new Error('forgecss: parse requires "css" as an argument.');
|
|
90
|
+
}
|
|
91
|
+
if (!html) {
|
|
92
|
+
throw new Error('forgecss: parse requires "html" as an argument.');
|
|
93
|
+
}
|
|
94
|
+
invalidateInvetory();
|
|
95
|
+
invalidateUsageCache();
|
|
96
|
+
// filling the inventory
|
|
97
|
+
try {
|
|
98
|
+
await extractStyles("styles.css", css);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`forgecss: error extracting styles.`, err);
|
|
101
|
+
}
|
|
102
|
+
// finding the usages
|
|
65
103
|
try {
|
|
66
|
-
await
|
|
104
|
+
await findUsages("usage.html", html);
|
|
67
105
|
} catch (err) {
|
|
68
|
-
console.error(`forgecss: error
|
|
106
|
+
console.error(`forgecss: error extracting usages.`, err);
|
|
69
107
|
}
|
|
108
|
+
return result(output);
|
|
70
109
|
}
|
|
71
110
|
};
|
|
72
111
|
}
|
package/lib/generator.js
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { getUsages } from "./processor.js";
|
|
2
|
+
import mediaQueryTransformer from "./transformers/mediaQuery.js";
|
|
3
|
+
import pseudoClassTransformer from "./transformers/pseudo.js";
|
|
4
4
|
|
|
5
5
|
export async function generateOutputCSS(config) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
Object.keys(
|
|
9
|
-
Object.keys(
|
|
6
|
+
const bucket = {};
|
|
7
|
+
const usages = getUsages();
|
|
8
|
+
Object.keys(usages).map((file) => {
|
|
9
|
+
Object.keys(usages[file]).forEach(async (label) => {
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
if (mediaQueryTransformer(config, label, usages[file][label], bucket)) {
|
|
12
|
+
return;
|
|
13
|
+
} else if (pseudoClassTransformer(label, usages[file][label], bucket)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
12
16
|
} catch (err) {
|
|
13
|
-
console.error(
|
|
17
|
+
console.error(
|
|
18
|
+
`forgecss: Error generating media query for label "${label}" (found in file ${file.replace(
|
|
19
|
+
process.cwd(),
|
|
20
|
+
""
|
|
21
|
+
)})`,
|
|
22
|
+
err
|
|
23
|
+
);
|
|
14
24
|
}
|
|
15
25
|
});
|
|
16
26
|
});
|
|
17
|
-
|
|
18
|
-
.map((
|
|
27
|
+
return Object.keys(bucket)
|
|
28
|
+
.map((key) => {
|
|
29
|
+
if (bucket[key].rules) {
|
|
30
|
+
return bucket[key].rules.toString();
|
|
31
|
+
}
|
|
32
|
+
return bucket[key].toString();
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean)
|
|
19
35
|
.join("\n");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`/* ForgeCSS autogenerated file */\n${result}`,
|
|
23
|
-
"utf-8"
|
|
24
|
-
);
|
|
25
|
-
}
|
|
36
|
+
}
|
|
37
|
+
|
package/lib/inventory.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import postcss from "postcss";
|
|
3
|
+
import safeParser from "postcss-safe-parser";
|
|
4
|
+
|
|
5
|
+
let INVENTORY = {};
|
|
6
|
+
|
|
7
|
+
export async function extractStyles(filePath, css = null) {
|
|
8
|
+
const content = css !== null ? css : await readFile(filePath, 'utf-8');
|
|
9
|
+
INVENTORY[filePath] = postcss.parse(content, { parser: safeParser });
|
|
10
|
+
}
|
|
11
|
+
export function getStylesByClassName(selector) {
|
|
12
|
+
const decls = [];
|
|
13
|
+
Object.keys(INVENTORY).forEach((filePath) => {
|
|
14
|
+
INVENTORY[filePath].walkRules((rule) => {
|
|
15
|
+
if (rule.selectors && rule.selectors.includes(`.${selector}`)) {
|
|
16
|
+
rule.walkDecls((d) => {
|
|
17
|
+
decls.push({ prop: d.prop, value: d.value, important: d.important });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
return decls;
|
|
23
|
+
}
|
|
24
|
+
export function invalidateInvetory() {
|
|
25
|
+
INVENTORY = {};
|
|
26
|
+
}
|
|
@@ -4,25 +4,26 @@ import { fromHtml } from "hast-util-from-html";
|
|
|
4
4
|
import { visit } from "unist-util-visit";
|
|
5
5
|
|
|
6
6
|
const FUNC_NAME = 'fx';
|
|
7
|
-
|
|
7
|
+
let USAGES = {};
|
|
8
8
|
|
|
9
9
|
const { parse } = swc;
|
|
10
10
|
|
|
11
|
-
export async function
|
|
12
|
-
const extension = filePath.split('.').pop().toLowerCase();
|
|
11
|
+
export async function findUsages(filePath, fileContent = null) {
|
|
13
12
|
try {
|
|
14
|
-
if (
|
|
13
|
+
if (USAGES[filePath]) {
|
|
14
|
+
// already processed
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
const content = await readFile(filePath, "utf-8");
|
|
17
|
+
USAGES[filePath] = {};
|
|
18
|
+
const content = fileContent ? fileContent : await readFile(filePath, "utf-8");
|
|
19
|
+
const extension = filePath.split('.').pop().toLowerCase();
|
|
19
20
|
|
|
20
21
|
// HTML
|
|
21
22
|
if (extension === "html") {
|
|
22
23
|
const ast = fromHtml(content);
|
|
23
24
|
visit(ast, "element", (node) => {
|
|
24
25
|
if (node.properties.className) {
|
|
25
|
-
|
|
26
|
+
storeUsage(filePath, node.properties.className.join(' '));
|
|
26
27
|
}
|
|
27
28
|
});
|
|
28
29
|
return;
|
|
@@ -34,7 +35,7 @@ export async function extractDeclarations(filePath) {
|
|
|
34
35
|
tsx: true,
|
|
35
36
|
decorators: false
|
|
36
37
|
});
|
|
37
|
-
|
|
38
|
+
traverseASTNode(ast, {
|
|
38
39
|
JSXExpressionContainer(node) {
|
|
39
40
|
if (node?.expression?.callee?.value === FUNC_NAME && node?.expression?.arguments) {
|
|
40
41
|
if (node?.expression?.arguments[0]) {
|
|
@@ -46,37 +47,44 @@ export async function extractDeclarations(filePath) {
|
|
|
46
47
|
value += elem?.cooked || "";
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
storeUsage(filePath, value);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
} catch (err) {
|
|
55
|
-
console.error(`forgecss: error processing file ${filePath}
|
|
56
|
+
console.error(`forgecss: error processing file ${filePath.replace(process.cwd(), '')}`, err);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
export function
|
|
59
|
-
if (
|
|
60
|
-
|
|
59
|
+
export function invalidateUsageCache(filePath) {
|
|
60
|
+
if (!filePath) {
|
|
61
|
+
USAGES = {};
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (USAGES[filePath]) {
|
|
65
|
+
delete USAGES[filePath];
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
|
-
function
|
|
68
|
+
export function getUsages() {
|
|
69
|
+
return USAGES;
|
|
70
|
+
}
|
|
71
|
+
function storeUsage(filePath, classesString = "") {
|
|
64
72
|
if (classesString) {
|
|
65
73
|
classesString.split(" ").forEach((part) => {
|
|
66
74
|
if (part.indexOf(":") > -1) {
|
|
67
75
|
let [label, classes] = part.split(":");
|
|
68
76
|
classes = classes.split(",");
|
|
69
77
|
classes.forEach((cls) => {
|
|
70
|
-
if (!
|
|
71
|
-
|
|
78
|
+
if (!USAGES[filePath][label]) {
|
|
79
|
+
USAGES[filePath][label] = [];
|
|
72
80
|
}
|
|
73
|
-
|
|
81
|
+
USAGES[filePath][label].push(cls);
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
});
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
|
-
function
|
|
87
|
+
function traverseASTNode(node, visitors, stack = []) {
|
|
80
88
|
if (!node || typeof node.type !== "string") {
|
|
81
89
|
return;
|
|
82
90
|
}
|
|
@@ -95,23 +103,20 @@ function traverseNode(node, visitors, stack = []) {
|
|
|
95
103
|
child.forEach((c) => {
|
|
96
104
|
if (c) {
|
|
97
105
|
if (typeof c.type === "string") {
|
|
98
|
-
|
|
106
|
+
traverseASTNode(c, visitors, [node].concat(stack));
|
|
99
107
|
} else if (c?.expression && typeof c.expression.type === "string") {
|
|
100
|
-
|
|
108
|
+
traverseASTNode(c.expression, visitors, [node].concat(stack));
|
|
101
109
|
} else if (c?.callee && typeof c.callee.type === "string") {
|
|
102
|
-
|
|
110
|
+
traverseASTNode(c.callee, visitors, [node].concat(stack));
|
|
103
111
|
} else if (c?.left && typeof c.left.type === "string") {
|
|
104
|
-
|
|
112
|
+
traverseASTNode(c.left, visitors, [node].concat(stack));
|
|
105
113
|
} else if (c?.right && typeof c.right.type === "string") {
|
|
106
|
-
|
|
114
|
+
traverseASTNode(c.right, visitors, [node].concat(stack));
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
});
|
|
110
118
|
} else if (child && typeof child.type === "string") {
|
|
111
|
-
|
|
119
|
+
traverseASTNode(child, visitors, [node].concat(stack));
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
|
-
}
|
|
115
|
-
export function getDeclarations() {
|
|
116
|
-
return DECLARATIONS;
|
|
117
122
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
|
|
3
|
+
import { getStylesByClassName } from "../inventory.js";
|
|
4
|
+
|
|
5
|
+
export default function mediaQueryTransformer(config, label, selectors, bucket) {
|
|
6
|
+
if (!config?.mapping?.queries[label]) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (!bucket[label]) {
|
|
10
|
+
bucket[label] = {
|
|
11
|
+
rules: postcss.atRule({
|
|
12
|
+
name: "media",
|
|
13
|
+
params: `all and (${config.mapping.queries[label]})`
|
|
14
|
+
}),
|
|
15
|
+
classes: {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const rules = bucket[label].rules;
|
|
19
|
+
selectors.forEach((selector) => {
|
|
20
|
+
const prefixedSelector = `.${label}_${selector}`;
|
|
21
|
+
if (bucket[label].classes[prefixedSelector]) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
bucket[label].classes[prefixedSelector] = true; // caching
|
|
25
|
+
const rule = postcss.rule({ selector: prefixedSelector });
|
|
26
|
+
const decls = getStylesByClassName(selector);
|
|
27
|
+
if (decls.length === 0) {
|
|
28
|
+
console.warn(`forgecss: no styles found for class ".${selector}" used in media query "${label}"`);
|
|
29
|
+
delete bucket[label];
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
decls.forEach((d) => {
|
|
33
|
+
rule.append(
|
|
34
|
+
postcss.decl({
|
|
35
|
+
prop: d.prop,
|
|
36
|
+
value: d.value,
|
|
37
|
+
important: d.important
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
rules.append(rule);
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
import { getStylesByClassName } from "../inventory.js";
|
|
3
|
+
|
|
4
|
+
const ALLOWED_PSEUDO_CLASSES = [
|
|
5
|
+
"hover",
|
|
6
|
+
"active",
|
|
7
|
+
"focus",
|
|
8
|
+
"focus-visible",
|
|
9
|
+
"focus-within",
|
|
10
|
+
"disabled",
|
|
11
|
+
"enabled",
|
|
12
|
+
"read-only",
|
|
13
|
+
"read-write",
|
|
14
|
+
"checked",
|
|
15
|
+
"indeterminate",
|
|
16
|
+
"valid",
|
|
17
|
+
"invalid",
|
|
18
|
+
"required",
|
|
19
|
+
"optional",
|
|
20
|
+
"in-range",
|
|
21
|
+
"out-of-range",
|
|
22
|
+
"placeholder-shown",
|
|
23
|
+
"autofill",
|
|
24
|
+
"user-invalid"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export default function pseudoClassTransformer(label, selectors, bucket) {
|
|
28
|
+
if (!ALLOWED_PSEUDO_CLASSES.includes(label)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const root = postcss.root();
|
|
32
|
+
selectors.forEach((selector) => {
|
|
33
|
+
const key = `${label}_${selector}`;
|
|
34
|
+
if (bucket[key]) {
|
|
35
|
+
// already have that
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const rule = postcss.rule({ selector: `.${key}:${label}` });
|
|
39
|
+
const decls = getStylesByClassName(selector);
|
|
40
|
+
if (decls.length === 0) {
|
|
41
|
+
console.warn(`forgecss: no styles found for class ".${selector}" used in pseudo class "${label}"`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
decls.forEach((d) => {
|
|
45
|
+
rule.append(
|
|
46
|
+
postcss.decl({
|
|
47
|
+
prop: d.prop,
|
|
48
|
+
value: d.value,
|
|
49
|
+
important: d.important
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
root.append(rule);
|
|
54
|
+
bucket[key] = root;
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgecss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ForgeCSS turns strings into fully generated responsive CSS using a custom DSL.",
|
|
6
6
|
"author": "Krasimir Tsonev",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "node ./scripts/build.js"
|
|
9
|
+
"build": "node ./scripts/build.js",
|
|
10
|
+
"test": "node ./tests/run.js"
|
|
10
11
|
},
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
package/tsconfig.json
ADDED
package/lib/styles.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import postcss from "postcss";
|
|
3
|
-
import safeParser from "postcss-safe-parser";
|
|
4
|
-
|
|
5
|
-
const STYLES = {};
|
|
6
|
-
|
|
7
|
-
export async function extractStyles(filePath) {
|
|
8
|
-
const content = await readFile(filePath, 'utf-8');
|
|
9
|
-
STYLES[filePath] = postcss.parse(content, { parser: safeParser });
|
|
10
|
-
}
|
|
11
|
-
export function getStylesByClassName(selector) {
|
|
12
|
-
const decls = [];
|
|
13
|
-
Object.keys(STYLES).forEach((filePath) => {
|
|
14
|
-
STYLES[filePath].walkRules((rule) => {
|
|
15
|
-
if (rule.selectors && rule.selectors.includes(`.${selector}`)) {
|
|
16
|
-
rule.walkDecls((d) => {
|
|
17
|
-
decls.push({ prop: d.prop, value: d.value, important: d.important });
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
return decls;
|
|
23
|
-
}
|
|
24
|
-
export function createMediaStyle(config, label, selectors, cache) {
|
|
25
|
-
if (!config.mapping.queries[label]) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`Unknown media query label: ${label}. Check app-fe/wwwroot/scripts/lib/generateMediaQueries.js for available mappings.`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
if (!cache[label]) {
|
|
31
|
-
cache[label] = {
|
|
32
|
-
mq: postcss.atRule({
|
|
33
|
-
name: "media",
|
|
34
|
-
params: `all and (${config.mapping.queries[label].query})`
|
|
35
|
-
}),
|
|
36
|
-
classes: {}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const mq = cache[label].mq;
|
|
40
|
-
selectors.forEach((selector) => {
|
|
41
|
-
const prefixedSelector = `.${label}_${selector}`;
|
|
42
|
-
if (cache[label].classes[prefixedSelector]) { return; }
|
|
43
|
-
cache[label].classes[prefixedSelector] = true;
|
|
44
|
-
const rule = postcss.rule({ selector: prefixedSelector });
|
|
45
|
-
const decls = getStylesByClassName(selector);
|
|
46
|
-
if (decls.length === 0) {
|
|
47
|
-
console.warn(`Warning: No styles found for class .${selector} used in media query ${label}`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
decls.forEach((d) => {
|
|
51
|
-
rule.append(
|
|
52
|
-
postcss.decl({
|
|
53
|
-
prop: d.prop,
|
|
54
|
-
value: d.value,
|
|
55
|
-
important: d.important
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
mq.append(rule);
|
|
60
|
-
});
|
|
61
|
-
}
|