forgecss 0.4.0 → 0.6.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 +2 -1
- package/client/fx.js +1 -1
- package/dist/forgecss.min.js +1 -1
- package/index.d.ts +6 -4
- package/index.js +61 -10
- package/lib/forge-lang/Compiler.js +122 -0
- package/lib/forge-lang/Parser.js +208 -0
- package/lib/forge-lang/constants.js +27 -0
- package/lib/forge-lang/utils.js +18 -0
- package/lib/helpers.js +27 -16
- package/lib/inventory.js +15 -7
- package/lib/usages.js +3 -24
- package/package.json +1 -1
- package/lib/generator.js +0 -42
- package/lib/getAllFiles.js +0 -30
- package/lib/inventory.d.ts +0 -4
- package/lib/transformers/arbitrary.js +0 -32
- package/lib/transformers/mediaQuery.js +0 -31
- package/lib/transformers/pseudo.js +0 -46
- package/lib/usages.d.ts +0 -3
package/cli.js
CHANGED
|
@@ -4,9 +4,10 @@ import fs from 'fs';
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { program } from "commander";
|
|
7
|
-
import ForgeCSS from './index.js';
|
|
8
7
|
import chokidar from "chokidar";
|
|
9
8
|
|
|
9
|
+
import ForgeCSS from './index.js';
|
|
10
|
+
|
|
10
11
|
program.option("-c, --config <string>,", "Path to forgecss config file", process.cwd() + "/forgecss.config.js");
|
|
11
12
|
program.option("-w, --watch", "Enable watch mode", false);
|
|
12
13
|
program.option("-v, --verbose", "Enable watch mode", false);
|
package/client/fx.js
CHANGED
package/dist/forgecss.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{function s(n){return a(n).map(t=>{let[e,i]=f(t);return!e||e==="[true]"?i:e==="[false]"?!1:(e=c(e),i.split(",").map(l=>`${e}
|
|
1
|
+
(()=>{function s(n){return a(n).map(t=>{let[e,i]=f(t);return!e||e==="[true]"?i:e==="[false]"?!1:(e=c(e),i.split(",").map(l=>`${e}_${l}`).join(" "))}).filter(Boolean).join(" ")}function f(n){let t=n.lastIndexOf(":");if(t===-1)return[null,n];let e=n.slice(0,t),i=n.slice(t+1);return[e,i]}function c(n){let t=n.trim();return t=t.replace(/[&]/g,"I"),t=t.replace(/[:| =]/g,"-"),t=t.replace(/[^a-zA-Z0-9_-]/g,""),t}function a(n){let t=[],e="",i=0,l=null;for(let r=0;r<n.length;r++){let o=n[r];if(i>0){if(l){e+=o,o===l&&n[r-1]!=="\\"&&(l=null);continue}else if(o==="'"||o==='"'){l=o,e+=o;continue}}if(o==="["){i++,e+=o;continue}if(o==="]"&&i>0){i--,e+=o;continue}if(i===0&&/\s/.test(o)){for(e&&t.push(e),e="";r+1<n.length&&/\s/.test(n[r+1]);)r++;continue}e+=o}return e&&t.push(e),t}function u(n){for(var t=n||document,e=t.querySelectorAll("[class]"),i=0;i<e.length;i++){var l=e[i],r=l.getAttribute("class");if(r){var o=s(r);typeof o=="string"&&o!==r&&l.setAttribute("class",o)}}}window.fx=s;window.forgecss=u;document.readyState!=="loading"?u():document.addEventListener("DOMContentLoaded",function(){u()});window.addEventListener("load",function(){u()});})();
|
package/index.d.ts
CHANGED
|
@@ -3,14 +3,16 @@ export type ForgeCSSOptions = {
|
|
|
3
3
|
usageFiles?: string[];
|
|
4
4
|
usageAttributes?: string[];
|
|
5
5
|
breakpoints?: {
|
|
6
|
-
[key: string]: string
|
|
6
|
+
[key: string]: string;
|
|
7
7
|
};
|
|
8
|
+
verbose?: boolean;
|
|
9
|
+
minify?: boolean;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
export type ForgeInstance = {
|
|
11
|
-
parseDirectory: (options: { dir: string; output?: string }) => Promise<string>;
|
|
12
|
-
parseFile: (options: { file: string; output?: string }) => Promise<string>;
|
|
13
|
-
parse: (options: { css: string; html?: string; jsx?: string; output?: string }) => Promise<string>;
|
|
13
|
+
parseDirectory: (options: { dir: string; output?: string; watch?: boolean }) => Promise<string>;
|
|
14
|
+
parseFile: (options: { file: string; output?: string; watch?: boolean }) => Promise<string>;
|
|
15
|
+
parse: (options: { css: string; html?: string; jsx?: string; output?: string; }) => Promise<string>;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
declare function ForgeCSS(options?: ForgeCSSOptions): ForgeInstance;
|
package/index.js
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { writeFile } from "fs/promises";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import chokidar from "chokidar";
|
|
4
|
+
|
|
5
|
+
import { extractStyles, getStylesByClassName, invalidateInventory, resolveApplys } from "./lib/inventory.js";
|
|
6
|
+
import { invalidateUsageCache, findUsages, getUsages } from "./lib/usages.js";
|
|
7
|
+
import { astToRules, rulesToCSS } from './lib/forge-lang/Compiler.js';
|
|
8
|
+
import { toAST } from './lib/forge-lang/Parser.js';
|
|
9
|
+
import { getAllFiles } from './lib/helpers.js';
|
|
6
10
|
|
|
7
11
|
const DEFAULT_OPTIONS = {
|
|
8
12
|
inventoryFiles: ["css", "less", "scss"],
|
|
9
13
|
usageFiles: ["html", "jsx", "tsx"],
|
|
10
14
|
usageAttributes: ["class", "className"],
|
|
11
|
-
breakpoints: {}
|
|
15
|
+
breakpoints: {},
|
|
16
|
+
verbose: true,
|
|
17
|
+
minify: true
|
|
12
18
|
};
|
|
13
19
|
|
|
14
20
|
export default function ForgeCSS(options) {
|
|
15
21
|
const config = { ...DEFAULT_OPTIONS };
|
|
16
22
|
|
|
17
23
|
config.breakpoints = Object.assign({}, DEFAULT_OPTIONS.breakpoints, options?.breakpoints ?? {});
|
|
24
|
+
config.inventoryFiles = options?.inventoryFiles ?? DEFAULT_OPTIONS.inventoryFiles;
|
|
25
|
+
config.usageFiles = options?.usageFiles ?? DEFAULT_OPTIONS.usageFiles;
|
|
26
|
+
config.usageAttributes = options?.usageAttributes ?? DEFAULT_OPTIONS.usageAttributes;
|
|
27
|
+
config.verbose = options?.verbose ?? DEFAULT_OPTIONS.verbose;
|
|
28
|
+
config.minify = options?.minify ?? DEFAULT_OPTIONS.minify;
|
|
18
29
|
|
|
19
30
|
async function result(output) {
|
|
20
31
|
try {
|
|
21
|
-
const
|
|
32
|
+
const cache = {};
|
|
33
|
+
const usages = getUsages();
|
|
34
|
+
const ast = toAST(
|
|
35
|
+
Object.values(usages).reduce((acc, i) => {
|
|
36
|
+
return acc.concat(i);
|
|
37
|
+
}, [])
|
|
38
|
+
);
|
|
39
|
+
let rules = astToRules(ast, {
|
|
40
|
+
getStylesByClassName,
|
|
41
|
+
cache,
|
|
42
|
+
config
|
|
43
|
+
});
|
|
44
|
+
rules.push(resolveApplys());
|
|
45
|
+
const css = rulesToCSS(rules.filter(Boolean), config);
|
|
22
46
|
if (output) {
|
|
23
|
-
await writeFile(output, `/* ForgeCSS
|
|
47
|
+
await writeFile(output, `/* ForgeCSS auto-generated file */\n${css}`, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
if (config.verbose) {
|
|
50
|
+
console.log("forgecss: Output CSS generated successfully.");
|
|
24
51
|
}
|
|
25
52
|
return css;
|
|
26
53
|
} catch (err) {
|
|
@@ -28,9 +55,27 @@ export default function ForgeCSS(options) {
|
|
|
28
55
|
}
|
|
29
56
|
return null;
|
|
30
57
|
}
|
|
58
|
+
function runWatcher(what, output, callback) {
|
|
59
|
+
const watcher = chokidar.watch(what, {
|
|
60
|
+
persistent: true,
|
|
61
|
+
ignoreInitial: true,
|
|
62
|
+
ignored: (p, stats) => output && path.resolve(p) === path.resolve(output)
|
|
63
|
+
});
|
|
64
|
+
watcher.on("change", async (filePath) => {
|
|
65
|
+
if (config.verbose) {
|
|
66
|
+
invalidateUsageCache(filePath)
|
|
67
|
+
invalidateInventory(filePath);
|
|
68
|
+
console.log(`forgecss: Detected change in ${filePath}`);
|
|
69
|
+
}
|
|
70
|
+
callback();
|
|
71
|
+
});
|
|
72
|
+
if (config.verbose) {
|
|
73
|
+
console.log("forgecss: Watch mode enabled. Listening for file changes...");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
31
76
|
|
|
32
77
|
return {
|
|
33
|
-
async parseDirectory({ dir, output = null }) {
|
|
78
|
+
async parseDirectory({ dir, output = null, watch = false }) {
|
|
34
79
|
if (!dir) {
|
|
35
80
|
throw new Error('forgecss: parseDirectory requires "dir" as an argument.');
|
|
36
81
|
}
|
|
@@ -52,10 +97,13 @@ export default function ForgeCSS(options) {
|
|
|
52
97
|
} catch (err) {
|
|
53
98
|
console.error(`forgecss: error extracting usages`, err);
|
|
54
99
|
}
|
|
100
|
+
watch && runWatcher(dir, output, () => {
|
|
101
|
+
this.parseDirectory({ dir, output, watch: false });
|
|
102
|
+
});
|
|
55
103
|
// generating the output CSS
|
|
56
104
|
return result(output);
|
|
57
105
|
},
|
|
58
|
-
async parseFile({ file, output = null }) {
|
|
106
|
+
async parseFile({ file, output = null, watch = false }) {
|
|
59
107
|
if (!file) {
|
|
60
108
|
throw new Error('forgecss: parseFile requires "file" as an argument.');
|
|
61
109
|
}
|
|
@@ -77,6 +125,9 @@ export default function ForgeCSS(options) {
|
|
|
77
125
|
} catch (err) {
|
|
78
126
|
console.error(`forgecss: error extracting usages.`, err);
|
|
79
127
|
}
|
|
128
|
+
watch && runWatcher(file, output, () => {
|
|
129
|
+
this.parseFile({ file, output, watch: false });
|
|
130
|
+
});
|
|
80
131
|
// generating the output CSS
|
|
81
132
|
return result(output);
|
|
82
133
|
},
|
|
@@ -87,7 +138,7 @@ export default function ForgeCSS(options) {
|
|
|
87
138
|
if (!html && !jsx) {
|
|
88
139
|
throw new Error('forgecss: parse requires "html" or "jsx".');
|
|
89
140
|
}
|
|
90
|
-
|
|
141
|
+
invalidateInventory();
|
|
91
142
|
invalidateUsageCache();
|
|
92
143
|
// filling the inventory
|
|
93
144
|
try {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
import { NODE_TYPE, ALLOWED_PSEUDO_CLASSES } from "./constants.js";
|
|
3
|
+
import { minifyCSS } from './utils.js'
|
|
4
|
+
import { normalizeLabel } from "../../client/fx.js";
|
|
5
|
+
|
|
6
|
+
export function astToRules(ast, options) {
|
|
7
|
+
let rules = [];
|
|
8
|
+
const { getStylesByClassName, cache = {}, config } = options
|
|
9
|
+
// console.log(
|
|
10
|
+
// "\n====================================================================== ^\n",
|
|
11
|
+
// JSON.stringify(ast, null, 2),
|
|
12
|
+
// "\n====================================================================== $\n"
|
|
13
|
+
// );
|
|
14
|
+
|
|
15
|
+
for(let node of ast) {
|
|
16
|
+
switch (node.type) {
|
|
17
|
+
case NODE_TYPE.TOKEN:
|
|
18
|
+
// ignoring ... just tokens
|
|
19
|
+
break;
|
|
20
|
+
case NODE_TYPE.VARIANT:
|
|
21
|
+
let variantSelector = node.selector;
|
|
22
|
+
let classes = (node?.payload?.value ?? "").split(",").map((c) => c.trim()).filter(Boolean);
|
|
23
|
+
let childRules;
|
|
24
|
+
if (!node.payload.value && typeof node.payload === 'object') {
|
|
25
|
+
childRules = astToRules([node.payload], options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// -------------------------------------------------------- pseudo
|
|
29
|
+
if (ALLOWED_PSEUDO_CLASSES.includes(variantSelector)) {
|
|
30
|
+
classes.forEach(cls => {
|
|
31
|
+
let selector = `.${variantSelector}_${cls}`;
|
|
32
|
+
const rule = createRule(`${selector}:${variantSelector}`, cls, cache);
|
|
33
|
+
if (rule) {
|
|
34
|
+
rules.push(rule);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// -------------------------------------------------------- media queries
|
|
38
|
+
} else if (config.breakpoints[variantSelector]) {
|
|
39
|
+
let mediaRule;
|
|
40
|
+
if (cache[config.breakpoints[variantSelector]]) {
|
|
41
|
+
mediaRule = cache[config.breakpoints[variantSelector]];
|
|
42
|
+
} else {
|
|
43
|
+
mediaRule = cache[config.breakpoints[variantSelector]] = postcss.atRule({
|
|
44
|
+
name: "media",
|
|
45
|
+
params: config.breakpoints[variantSelector]
|
|
46
|
+
});
|
|
47
|
+
rules.push(mediaRule);
|
|
48
|
+
}
|
|
49
|
+
if (childRules) {
|
|
50
|
+
childRules.forEach(r => {
|
|
51
|
+
mediaRule.append(r);
|
|
52
|
+
})
|
|
53
|
+
} else {
|
|
54
|
+
classes.forEach((cls) => {
|
|
55
|
+
let selector = `.${variantSelector}_${cls}`;
|
|
56
|
+
const rule = createRule(selector, cls, cache);
|
|
57
|
+
if (rule) {
|
|
58
|
+
mediaRule.append(rule);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} else if (node.payload?.type === NODE_TYPE.TOKEN && node.simple === true) {
|
|
63
|
+
console.warn(`forgecss: there is no breakpoint defined for label "${variantSelector}".`);
|
|
64
|
+
// -------------------------------------------------------- arbitrary
|
|
65
|
+
} else {
|
|
66
|
+
classes.forEach(cls => {
|
|
67
|
+
if (Array.isArray(variantSelector)) {
|
|
68
|
+
variantSelector = variantSelector
|
|
69
|
+
.map(({ type, value, selector, payload }) => {
|
|
70
|
+
if (type === "token") {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(" ");
|
|
76
|
+
}
|
|
77
|
+
if (["", "true"].includes(variantSelector)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const I = normalizeLabel(variantSelector) + "_" + cls;
|
|
81
|
+
const selector = evaluateArbitrary(variantSelector, I);
|
|
82
|
+
const rule = createRule(selector, cls, cache);
|
|
83
|
+
if (rule) {
|
|
84
|
+
rules.push(rule);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createRule(selector, pickStylesFrom, cache = {}) {
|
|
93
|
+
if (cache[selector]) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const newRule = cache[selector] = postcss.rule({ selector });
|
|
97
|
+
const decls = getStylesByClassName(pickStylesFrom);
|
|
98
|
+
if (decls.length === 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
decls.forEach((d) => {
|
|
102
|
+
newRule.append(
|
|
103
|
+
postcss.decl({
|
|
104
|
+
prop: d.prop,
|
|
105
|
+
value: d.value,
|
|
106
|
+
important: d.important
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
return newRule;
|
|
111
|
+
}
|
|
112
|
+
function evaluateArbitrary(variant, I) {
|
|
113
|
+
variant = variant.replace(/[&]/g, `.${I}`);
|
|
114
|
+
return variant;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return rules;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function rulesToCSS(rules, { minify } = { minify: true }) {
|
|
121
|
+
return minify ? minifyCSS(rules.map((r) => r.toString()).join("")) : rules.map((r) => r.toString()).join("\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
export function toAST(input, cache = {}) {
|
|
2
|
+
if (cache[input]) return cache[input];
|
|
3
|
+
|
|
4
|
+
if (Array.isArray(input)) {
|
|
5
|
+
const optimized = [];
|
|
6
|
+
input.forEach((str) => {
|
|
7
|
+
str
|
|
8
|
+
.trim()
|
|
9
|
+
.split(" ")
|
|
10
|
+
.forEach((part) => {
|
|
11
|
+
if (!optimized.includes(part)) optimized.push(part);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
input = optimized.join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const s = String(input ?? "").trim();
|
|
18
|
+
let i = 0;
|
|
19
|
+
|
|
20
|
+
const isWS = (ch) => ch === " " || ch === "\n" || ch === "\t" || ch === "\r";
|
|
21
|
+
|
|
22
|
+
function skipWS() {
|
|
23
|
+
while (i < s.length && isWS(s[i])) i++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseSequence(stopChar) {
|
|
27
|
+
const nodes = [];
|
|
28
|
+
while (i < s.length) {
|
|
29
|
+
skipWS();
|
|
30
|
+
if (stopChar && s[i] === stopChar) break;
|
|
31
|
+
if (i >= s.length) break;
|
|
32
|
+
nodes.push(parseItem());
|
|
33
|
+
}
|
|
34
|
+
return nodes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readIdentUntilDelimiter() {
|
|
38
|
+
let out = "";
|
|
39
|
+
while (i < s.length) {
|
|
40
|
+
const ch = s[i];
|
|
41
|
+
// stop at whitespace, "(", ")", ":" (variant separator)
|
|
42
|
+
if (isWS(ch) || ch === "(" || ch === ")" || ch === ":") break;
|
|
43
|
+
// IMPORTANT: DO NOT consume "[" here; it may be:
|
|
44
|
+
// - leading bracket variant (handled in parseItem when ch === "[")
|
|
45
|
+
// - attribute selector suffix (handled in parseItem after reading head)
|
|
46
|
+
if (ch === "[") break;
|
|
47
|
+
|
|
48
|
+
out += ch;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
return out.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isVariantLabel(str) {
|
|
55
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isCallName(str) {
|
|
59
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(str);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseItem() {
|
|
63
|
+
skipWS();
|
|
64
|
+
const ch = s[i];
|
|
65
|
+
|
|
66
|
+
// Bracket variant: [selector]:payload
|
|
67
|
+
if (ch === "[") {
|
|
68
|
+
const selectorRaw = parseBracketContent(); // returns content WITHOUT outer []
|
|
69
|
+
const selectorAst = toAST(selectorRaw, cache);
|
|
70
|
+
|
|
71
|
+
if (s[i] === ":") {
|
|
72
|
+
i++;
|
|
73
|
+
const payload = parseItem();
|
|
74
|
+
return { type: "variant", selector: selectorAst, payload };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If it's just a standalone bracket chunk (not a variant),
|
|
78
|
+
// keep it as a token string. (You can change this if you prefer AST here.)
|
|
79
|
+
return { type: "token", value: `[${selectorRaw}]` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Read label/name/token
|
|
83
|
+
let head = readIdentUntilDelimiter();
|
|
84
|
+
|
|
85
|
+
// NEW: absorb attribute selector suffixes: foo[...][...]
|
|
86
|
+
// This handles &\[type=...\] and similar.
|
|
87
|
+
while (s[i] === "[") {
|
|
88
|
+
const inner = parseBracketContent(); // consumes the bracket block
|
|
89
|
+
head += `[${inner}]`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Label variant: hover:..., desktop:..., focus:...
|
|
93
|
+
if (s[i] === ":" && isVariantLabel(head)) {
|
|
94
|
+
i++; // consume ":"
|
|
95
|
+
const payload = parseItem();
|
|
96
|
+
return { type: "variant", selector: head, payload, simple: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Call: name(...)
|
|
100
|
+
if (s[i] === "(" && isCallName(head)) {
|
|
101
|
+
i++; // consume "("
|
|
102
|
+
const args = [];
|
|
103
|
+
while (i < s.length) {
|
|
104
|
+
skipWS();
|
|
105
|
+
if (s[i] === ")") {
|
|
106
|
+
i++;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
args.push(parseItem());
|
|
110
|
+
skipWS();
|
|
111
|
+
if (s[i] === ",") i++;
|
|
112
|
+
}
|
|
113
|
+
return { type: "call", name: head, args };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (s[i] === ":") {
|
|
117
|
+
head += ":";
|
|
118
|
+
i++; // consume ":"
|
|
119
|
+
|
|
120
|
+
// absorb following identifier / call / selector chunk
|
|
121
|
+
while (i < s.length) {
|
|
122
|
+
const ch = s[i];
|
|
123
|
+
if (isWS(ch) || ch === ")" || ch === ",") break;
|
|
124
|
+
|
|
125
|
+
if (ch === "[") {
|
|
126
|
+
const inner = parseBracketContent();
|
|
127
|
+
head += `[${inner}]`;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ch === "(") {
|
|
132
|
+
head += "(";
|
|
133
|
+
i++;
|
|
134
|
+
let depth = 1;
|
|
135
|
+
while (i < s.length && depth > 0) {
|
|
136
|
+
if (s[i] === "(") depth++;
|
|
137
|
+
if (s[i] === ")") depth--;
|
|
138
|
+
head += s[i++];
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
head += ch;
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { type: "token", value: head };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseBracketContent() {
|
|
152
|
+
// assumes s[i] === "["
|
|
153
|
+
i++; // consume "["
|
|
154
|
+
let out = "";
|
|
155
|
+
let bracket = 1;
|
|
156
|
+
let quote = null;
|
|
157
|
+
|
|
158
|
+
while (i < s.length) {
|
|
159
|
+
const ch = s[i];
|
|
160
|
+
|
|
161
|
+
if (quote) {
|
|
162
|
+
out += ch;
|
|
163
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
164
|
+
i++;
|
|
165
|
+
out += s[i];
|
|
166
|
+
} else if (ch === quote) {
|
|
167
|
+
quote = null;
|
|
168
|
+
}
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (ch === "'" || ch === '"') {
|
|
174
|
+
quote = ch;
|
|
175
|
+
out += ch;
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (ch === "[") {
|
|
181
|
+
bracket++;
|
|
182
|
+
out += ch;
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (ch === "]") {
|
|
188
|
+
bracket--;
|
|
189
|
+
if (bracket === 0) {
|
|
190
|
+
i++; // consume final "]"
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
out += ch;
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
out += ch;
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ast = parseSequence(null);
|
|
206
|
+
cache[input] = ast;
|
|
207
|
+
return ast;
|
|
208
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const NODE_TYPE = {
|
|
2
|
+
VARIANT: "variant",
|
|
3
|
+
CALL: "call",
|
|
4
|
+
TOKEN: "token"
|
|
5
|
+
};
|
|
6
|
+
export const ALLOWED_PSEUDO_CLASSES = [
|
|
7
|
+
"hover",
|
|
8
|
+
"active",
|
|
9
|
+
"focus",
|
|
10
|
+
"focus-visible",
|
|
11
|
+
"focus-within",
|
|
12
|
+
"disabled",
|
|
13
|
+
"enabled",
|
|
14
|
+
"read-only",
|
|
15
|
+
"read-write",
|
|
16
|
+
"checked",
|
|
17
|
+
"indeterminate",
|
|
18
|
+
"valid",
|
|
19
|
+
"invalid",
|
|
20
|
+
"required",
|
|
21
|
+
"optional",
|
|
22
|
+
"in-range",
|
|
23
|
+
"out-of-range",
|
|
24
|
+
"placeholder-shown",
|
|
25
|
+
"autofill",
|
|
26
|
+
"user-invalid"
|
|
27
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function minifyCSS(css) {
|
|
2
|
+
return (
|
|
3
|
+
css
|
|
4
|
+
// remove comments
|
|
5
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
6
|
+
// remove whitespace around symbols
|
|
7
|
+
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
8
|
+
// remove trailing semicolons
|
|
9
|
+
.replace(/;}/g, "}")
|
|
10
|
+
// collapse multiple spaces
|
|
11
|
+
.replace(/\s+/g, " ")
|
|
12
|
+
// remove spaces before/after braces
|
|
13
|
+
.replace(/\s*{\s*/g, "{")
|
|
14
|
+
.replace(/\s*}\s*/g, "}")
|
|
15
|
+
// trim
|
|
16
|
+
.trim()
|
|
17
|
+
);
|
|
18
|
+
}
|
package/lib/helpers.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
3
|
|
|
4
|
-
export function
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export async function getAllFiles(dir, matchFiles) {
|
|
5
|
+
const result = [];
|
|
6
|
+
const stack = [dir];
|
|
7
|
+
|
|
8
|
+
while (stack.length > 0) {
|
|
9
|
+
const currentDir = stack.pop();
|
|
10
|
+
|
|
11
|
+
let dirHandle;
|
|
12
|
+
try {
|
|
13
|
+
dirHandle = await fs.opendir(currentDir);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for await (const entry of dirHandle) {
|
|
19
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
20
|
+
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
stack.push(fullPath);
|
|
23
|
+
} else if (matchFiles.includes(fullPath.split(".").pop()?.toLowerCase())) {
|
|
24
|
+
result.push(fullPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
postcss.decl({
|
|
13
|
-
prop: d.prop,
|
|
14
|
-
value: d.value,
|
|
15
|
-
important: d.important
|
|
16
|
-
})
|
|
17
|
-
);
|
|
18
|
-
});
|
|
28
|
+
|
|
29
|
+
return result;
|
|
19
30
|
}
|
package/lib/inventory.js
CHANGED
|
@@ -20,14 +20,21 @@ export function getStylesByClassName(selector) {
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
if (decls.length === 0) {
|
|
23
|
-
console.warn(`forgecss:
|
|
23
|
+
console.warn(`forgecss: no styles found for class "${selector}".`);
|
|
24
24
|
}
|
|
25
25
|
return decls;
|
|
26
26
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
27
|
+
export function invalidateInventory(filePath) {
|
|
28
|
+
if (!filePath) {
|
|
29
|
+
INVENTORY = {};
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (INVENTORY[filePath]) {
|
|
33
|
+
delete INVENTORY[filePath];
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
|
-
export function resolveApplys(
|
|
36
|
+
export function resolveApplys() {
|
|
37
|
+
let resolvedApplies;
|
|
31
38
|
Object.keys(INVENTORY).forEach((filePath) => {
|
|
32
39
|
INVENTORY[filePath].walkRules((rule) => {
|
|
33
40
|
rule.walkDecls((d) => {
|
|
@@ -44,12 +51,13 @@ export function resolveApplys(bucket) {
|
|
|
44
51
|
});
|
|
45
52
|
});
|
|
46
53
|
});
|
|
47
|
-
if (!
|
|
48
|
-
|
|
54
|
+
if (!resolvedApplies) {
|
|
55
|
+
resolvedApplies = postcss.root();
|
|
49
56
|
}
|
|
50
|
-
|
|
57
|
+
resolvedApplies.append(newRule);
|
|
51
58
|
}
|
|
52
59
|
});
|
|
53
60
|
});
|
|
54
61
|
});
|
|
62
|
+
return resolvedApplies;
|
|
55
63
|
}
|
package/lib/usages.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function findUsages(filePath, fileContent = null) {
|
|
|
15
15
|
// already processed
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
|
-
USAGES[filePath] =
|
|
18
|
+
USAGES[filePath] = [];
|
|
19
19
|
const content = fileContent ? fileContent : await readFile(filePath, "utf-8");
|
|
20
20
|
const extension = filePath.split('.').pop().toLowerCase();
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ export async function findUsages(filePath, fileContent = null) {
|
|
|
24
24
|
const ast = fromHtml(content);
|
|
25
25
|
visit(ast, "element", (node) => {
|
|
26
26
|
if (node.properties.className) {
|
|
27
|
-
|
|
27
|
+
USAGES[filePath].push(node.properties.className.join(" "));
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
return;
|
|
@@ -47,7 +47,7 @@ export async function findUsages(filePath, fileContent = null) {
|
|
|
47
47
|
let quasis = arg.expression.quasis.map((elem) => elem?.cooked || "");
|
|
48
48
|
value = quasis.join("");
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
USAGES[filePath].push(value);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -68,27 +68,6 @@ export function invalidateUsageCache(filePath) {
|
|
|
68
68
|
export function getUsages() {
|
|
69
69
|
return USAGES;
|
|
70
70
|
}
|
|
71
|
-
function storeUsage(filePath, classesString = "") {
|
|
72
|
-
if (!classesString) return;
|
|
73
|
-
|
|
74
|
-
parseClass(classesString).forEach((part) => {
|
|
75
|
-
if (part.includes(":")) {
|
|
76
|
-
const lastColonIndex = part.lastIndexOf(":");
|
|
77
|
-
const label = part.slice(0, lastColonIndex); // "desktop" or "[&:hover]"
|
|
78
|
-
const clsPart = part.slice(lastColonIndex + 1); // e.g. "mt1"
|
|
79
|
-
const classes = clsPart.split(",");
|
|
80
|
-
|
|
81
|
-
if (label === "[]") return;
|
|
82
|
-
|
|
83
|
-
if (!USAGES[filePath][label]) {
|
|
84
|
-
USAGES[filePath][label] = [];
|
|
85
|
-
}
|
|
86
|
-
classes.forEach((cls) => {
|
|
87
|
-
USAGES[filePath][label].push(cls);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
71
|
function traverseASTNode(node, visitors, stack = []) {
|
|
93
72
|
if (!node || typeof node.type !== "string") {
|
|
94
73
|
return;
|
package/package.json
CHANGED
package/lib/generator.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { getUsages } from "./usages.js";
|
|
2
|
-
import arbitraryTransformer from "./transformers/arbitrary.js";
|
|
3
|
-
import mediaQueryTransformer from "./transformers/mediaQuery.js";
|
|
4
|
-
import pseudoClassTransformer from "./transformers/pseudo.js";
|
|
5
|
-
import { resolveApplys } from "./inventory.js";
|
|
6
|
-
|
|
7
|
-
export async function generateOutputCSS(config) {
|
|
8
|
-
const bucket = {};
|
|
9
|
-
const usages = getUsages();
|
|
10
|
-
Object.keys(usages).map((file) => {
|
|
11
|
-
Object.keys(usages[file]).forEach(async (label) => {
|
|
12
|
-
try {
|
|
13
|
-
if (mediaQueryTransformer(config, label, usages[file][label], bucket)) {
|
|
14
|
-
return;
|
|
15
|
-
} else if (pseudoClassTransformer(label, usages[file][label], bucket)) {
|
|
16
|
-
return;
|
|
17
|
-
} else if (arbitraryTransformer(label, usages[file][label], bucket)) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error(
|
|
22
|
-
`forgecss: Error generating media query for label "${label}" (found in file ${file.replace(
|
|
23
|
-
process.cwd(),
|
|
24
|
-
""
|
|
25
|
-
)})`,
|
|
26
|
-
err
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
resolveApplys(bucket);
|
|
32
|
-
return Object.keys(bucket)
|
|
33
|
-
.map((key) => {
|
|
34
|
-
if (bucket[key].rules) {
|
|
35
|
-
return bucket[key].rules.toString();
|
|
36
|
-
}
|
|
37
|
-
return bucket[key].toString();
|
|
38
|
-
})
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
.join("\n");
|
|
41
|
-
}
|
|
42
|
-
|
package/lib/getAllFiles.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
export default async function getAllFiles(dir, matchFiles) {
|
|
5
|
-
const result = [];
|
|
6
|
-
const stack = [dir];
|
|
7
|
-
|
|
8
|
-
while (stack.length > 0) {
|
|
9
|
-
const currentDir = stack.pop();
|
|
10
|
-
|
|
11
|
-
let dirHandle;
|
|
12
|
-
try {
|
|
13
|
-
dirHandle = await fs.opendir(currentDir);
|
|
14
|
-
} catch (err) {
|
|
15
|
-
throw err;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
for await (const entry of dirHandle) {
|
|
19
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
20
|
-
|
|
21
|
-
if (entry.isDirectory()) {
|
|
22
|
-
stack.push(fullPath);
|
|
23
|
-
} else if (matchFiles.includes(fullPath.split(".").pop()?.toLowerCase())) {
|
|
24
|
-
result.push(fullPath);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return result;
|
|
30
|
-
}
|
package/lib/inventory.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import postcss from "postcss";
|
|
2
|
-
|
|
3
|
-
import { normalizeLabel } from "../../client/fx.js";
|
|
4
|
-
import { setDeclarations } from "../helpers.js";
|
|
5
|
-
|
|
6
|
-
export default function arbitraryTransformer(label, classes, bucket) {
|
|
7
|
-
if (label.startsWith("[") && label.endsWith("]")) {
|
|
8
|
-
let arbitrarySelector = label.slice(1, -1).trim();
|
|
9
|
-
if (['', 'true'].includes(arbitrarySelector)) {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
classes.forEach((cls) => {
|
|
13
|
-
const I = normalizeLabel(label) + "--" + cls;
|
|
14
|
-
const selector = evaluateArbitrary(arbitrarySelector, I);
|
|
15
|
-
const root = postcss.root();
|
|
16
|
-
if (bucket[I]) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const rule = postcss.rule({ selector });
|
|
20
|
-
setDeclarations(cls, rule);
|
|
21
|
-
root.append(rule);
|
|
22
|
-
bucket[I] = root;
|
|
23
|
-
});
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function evaluateArbitrary(label, I) {
|
|
30
|
-
label = label.replace(/[&]/g, `.${I}`);
|
|
31
|
-
return label;
|
|
32
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import postcss from "postcss";
|
|
2
|
-
|
|
3
|
-
import { setDeclarations } from "../helpers.js";
|
|
4
|
-
import {normalizeLabel} from "../../client/fx.js";
|
|
5
|
-
|
|
6
|
-
export default function mediaQueryTransformer(config, label, classes, bucket) {
|
|
7
|
-
if (!config?.breakpoints[label]) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
if (!bucket[label]) {
|
|
11
|
-
bucket[label] = {
|
|
12
|
-
rules: postcss.atRule({
|
|
13
|
-
name: "media",
|
|
14
|
-
params: `all and (${config.breakpoints[label]})`
|
|
15
|
-
}),
|
|
16
|
-
classes: {}
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
const rules = bucket[label].rules;
|
|
20
|
-
classes.forEach((cls) => {
|
|
21
|
-
const selector = `.${normalizeLabel(label)}--${cls}`;
|
|
22
|
-
if (bucket[label].classes[selector]) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
bucket[label].classes[selector] = true; // caching
|
|
26
|
-
const rule = postcss.rule({ selector });
|
|
27
|
-
setDeclarations(cls, rule);
|
|
28
|
-
rules.append(rule);
|
|
29
|
-
});
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import postcss from "postcss";
|
|
2
|
-
|
|
3
|
-
import {setDeclarations} from "../helpers.js";
|
|
4
|
-
import {normalizeLabel} from "../../client/fx.js";
|
|
5
|
-
|
|
6
|
-
const ALLOWED_PSEUDO_CLASSES = [
|
|
7
|
-
"hover",
|
|
8
|
-
"active",
|
|
9
|
-
"focus",
|
|
10
|
-
"focus-visible",
|
|
11
|
-
"focus-within",
|
|
12
|
-
"disabled",
|
|
13
|
-
"enabled",
|
|
14
|
-
"read-only",
|
|
15
|
-
"read-write",
|
|
16
|
-
"checked",
|
|
17
|
-
"indeterminate",
|
|
18
|
-
"valid",
|
|
19
|
-
"invalid",
|
|
20
|
-
"required",
|
|
21
|
-
"optional",
|
|
22
|
-
"in-range",
|
|
23
|
-
"out-of-range",
|
|
24
|
-
"placeholder-shown",
|
|
25
|
-
"autofill",
|
|
26
|
-
"user-invalid"
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
export default function pseudoClassTransformer(label, classes, bucket) {
|
|
30
|
-
if (ALLOWED_PSEUDO_CLASSES.includes(label)) {
|
|
31
|
-
classes.forEach((cls) => {
|
|
32
|
-
const selector = `.${normalizeLabel(label)}--${cls}:${label}`;
|
|
33
|
-
const root = postcss.root();
|
|
34
|
-
if (bucket[selector]) {
|
|
35
|
-
// already have that
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const rule = postcss.rule({ selector });
|
|
39
|
-
setDeclarations(cls, rule);
|
|
40
|
-
root.append(rule);
|
|
41
|
-
bucket[selector] = root;
|
|
42
|
-
});
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
package/lib/usages.d.ts
DELETED