forgecss 0.0.1 → 0.1.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/client/fx.js ADDED
@@ -0,0 +1,16 @@
1
+ export default function fx(classes) {
2
+ return classes
3
+ .split(" ")
4
+ .map((className) => {
5
+ const [label, rest] = className.split(":");
6
+ if (!rest) return label;
7
+ return rest
8
+ .split(",")
9
+ .map((cls) => `${label}_${cls}`)
10
+ .filter(Boolean)
11
+ .join(" ");
12
+ })
13
+ .filter(Boolean)
14
+ .join(" ");
15
+ };
16
+
@@ -0,0 +1,34 @@
1
+ import fxFn from "./fx.js";
2
+
3
+ function forgecss(root) {
4
+ var rootNode = root || document;
5
+
6
+ // Query only nodes that actually have "class" attributes
7
+ var nodes = rootNode.querySelectorAll('[class]');
8
+
9
+ for (var i = 0; i < nodes.length; i++) {
10
+ var el = nodes[i];
11
+ var original = el.getAttribute('class');
12
+ if (!original) continue;
13
+
14
+ var transformed = fxFn(original);
15
+
16
+ if (typeof transformed === 'string' && transformed !== original) {
17
+ el.setAttribute('class', transformed);
18
+ }
19
+ }
20
+ }
21
+
22
+ window.fx = fxFn;
23
+ window.forgecss = forgecss;
24
+
25
+ if (document.readyState !== 'loading') {
26
+ forgecss();
27
+ } else {
28
+ document.addEventListener('DOMContentLoaded', function () {
29
+ forgecss();
30
+ });
31
+ }
32
+ window.addEventListener('load', function () {
33
+ forgecss();
34
+ });
@@ -0,0 +1 @@
1
+ (()=>{function o(i){return i.split(" ").map(s=>{let[e,t]=s.split(":");return t?t.split(",").map(n=>`${e}_${n}`).filter(Boolean).join(" "):e}).filter(Boolean).join(" ")}function r(i){for(var s=i||document,e=s.querySelectorAll("[class]"),t=0;t<e.length;t++){var n=e[t],a=n.getAttribute("class");if(a){var f=o(a);typeof f=="string"&&f!==a&&n.setAttribute("class",f)}}}window.fx=o;window.forgecss=r;document.readyState!=="loading"?r():document.addEventListener("DOMContentLoaded",function(){r()});window.addEventListener("load",function(){r()});})();
package/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type ForgeCSSOptions = {
2
+ styles: {
3
+ sourceDir: string;
4
+ match?: string[];
5
+ };
6
+ ui: {
7
+ sourceDir: string;
8
+ match?: string[];
9
+ attributes?: string[];
10
+ };
11
+ mapping: {
12
+ queries: {
13
+ [key: string]: {
14
+ query: string;
15
+ };
16
+ };
17
+ };
18
+ output: string
19
+ };
20
+
21
+ export default function forgecss(options?: ForgeCSSOptions): {
22
+ parse: () => Promise<void>;
23
+ };
package/index.js CHANGED
@@ -0,0 +1,71 @@
1
+ import getAllFiles from "./lib/getAllFiles.js";
2
+ import { extractStyles } from "./lib/styles.js";
3
+ import { extractDeclarations } from "./lib/processFile.js";
4
+ import { generateOutputCSS } from "./lib/generator.js";
5
+ import forgeCSSExpressionTransformer from './client/fx.js';
6
+
7
+ export const fx = forgeCSSExpressionTransformer;
8
+
9
+ const DEFAULT_OPTIONS = {
10
+ styles: {
11
+ sourceDir: null,
12
+ match: ['css']
13
+ },
14
+ ui: {
15
+ sourceDir: null,
16
+ match: ["html", "jsx", "tsx"],
17
+ attribute: ['class', 'className']
18
+ },
19
+ mapping: {
20
+ queries: {}
21
+ },
22
+ output: null
23
+ };
24
+
25
+ export default function forgecss(options = { styles: {}, ui: {}, mapping: {}, output: null }) {
26
+ const config = { ...DEFAULT_OPTIONS };
27
+
28
+ config.styles = Object.assign({}, DEFAULT_OPTIONS.styles, options.styles || {});
29
+ config.ui = Object.assign({}, DEFAULT_OPTIONS.ui, options.ui || {});
30
+ config.mapping = Object.assign({}, DEFAULT_OPTIONS.mapping, options.mapping || {});
31
+ config.output = options.output || DEFAULT_OPTIONS.output;
32
+
33
+ if (!config.styles.sourceDir) {
34
+ throw new Error('forgecss: "styles.sourceDir" option is required.');
35
+ }
36
+ if (!config.ui.sourceDir) {
37
+ throw new Error('forgecss: "ui.sourceDir" option is required.');
38
+ }
39
+ if (!config.output) {
40
+ throw new Error('forgecss: "output" option is required.');
41
+ }
42
+
43
+ return {
44
+ async parse() {
45
+ // fetching the styles
46
+ try {
47
+ let files = await getAllFiles(config.styles.sourceDir, config.styles.match);
48
+ for (let file of files) {
49
+ await extractStyles(file);
50
+ }
51
+ } catch (err) {
52
+ console.error(`forgecss: error extracting styles: ${err}`);
53
+ }
54
+ // fetching the declarations
55
+ try {
56
+ let files = await getAllFiles(config.ui.sourceDir, config.ui.match);
57
+ for (let file of files) {
58
+ await extractDeclarations(file);
59
+ }
60
+ } catch(err) {
61
+ console.error(`forgecss: error extracting declarations: ${err}`);
62
+ }
63
+ // generating the output CSS
64
+ try {
65
+ await generateOutputCSS(config);
66
+ } catch(err) {
67
+ console.error(`forgecss: error generating output CSS: ${err}`);
68
+ }
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,26 @@
1
+ import { writeFile } from "fs/promises";
2
+ const OUTPUT = "forgecss-media-queries.css";
3
+ import { getDeclarations } from "./processFile.js";
4
+ import { createMediaStyle, extractStyles } from "./styles.js";
5
+
6
+ export async function generateOutputCSS(config) {
7
+ const cache = {};
8
+ const declarations = getDeclarations();
9
+ Object.keys(declarations).map((file) => {
10
+ Object.keys(declarations[file]).forEach(async (label) => {
11
+ try {
12
+ createMediaStyle(config, label, declarations[file][label], cache);
13
+ } catch (err) {
14
+ console.error(`Error generating media query for label ${label} in file ${file}: ${err}`);
15
+ }
16
+ });
17
+ });
18
+ const result = Object.keys(cache)
19
+ .map((label) => cache[label].mq.toString())
20
+ .join("\n");
21
+ await writeFile(
22
+ config.output,
23
+ `${result}`,
24
+ "utf-8"
25
+ );
26
+ }
@@ -0,0 +1,30 @@
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
+ }
@@ -0,0 +1,114 @@
1
+ import swc from "@swc/core";
2
+ import { readFile } from "fs/promises";
3
+ import { fromHtml } from "hast-util-from-html";
4
+ import { visit } from "unist-util-visit";
5
+
6
+ const FUNC_NAME = 'mq';
7
+ const DECLARATIONS = {};
8
+
9
+ const { parse } = swc;
10
+
11
+ export async function extractDeclarations(filePath) {
12
+ const extension = filePath.split('.').pop().toLowerCase();
13
+ try {
14
+ if (DECLARATIONS[filePath]) {
15
+ return;
16
+ }
17
+ DECLARATIONS[filePath] = {};
18
+ const content = await readFile(filePath, "utf-8");
19
+
20
+ // HTML
21
+ if (extension === "html") {
22
+ const ast = fromHtml(content);
23
+ visit(ast, "element", (node) => {
24
+ if (node.properties.className) {
25
+ pushToDeclarations(filePath, node.properties.className.join(' '));
26
+ }
27
+ });
28
+ return;
29
+ }
30
+
31
+ // JSX/TSX
32
+ const ast = await parse(content, {
33
+ syntax: "typescript",
34
+ tsx: true,
35
+ decorators: false
36
+ });
37
+ traverseNode(ast, {
38
+ JSXExpressionContainer(node) {
39
+ if (node?.expression?.callee?.value === FUNC_NAME && node?.expression?.arguments) {
40
+ if (node?.expression?.arguments[0]) {
41
+ const arg = node.expression.arguments[0];
42
+ let value = arg?.expression.value;
43
+ if (arg.expression.type === "TemplateLiteral") {
44
+ value = "";
45
+ arg.expression.quasis.forEach((elem) => {
46
+ value += elem?.cooked || "";
47
+ });
48
+ }
49
+ pushToDeclarations(filePath, value);
50
+ }
51
+ }
52
+ }
53
+ });
54
+ } catch (err) {
55
+ console.error(`forgecss: error processing file ${filePath}: ${err}`);
56
+ }
57
+ }
58
+ function pushToDeclarations(filePath, classesString = "") {
59
+ if (classesString) {
60
+ classesString.split(" ").forEach((part) => {
61
+ if (part.indexOf(":") > -1) {
62
+ let [label, classes] = part.split(":");
63
+ classes = classes.split(",");
64
+ classes.forEach((cls) => {
65
+ if (!DECLARATIONS[filePath][label]) {
66
+ DECLARATIONS[filePath][label] = [];
67
+ }
68
+ DECLARATIONS[filePath][label].push(cls);
69
+ });
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ function traverseNode(node, visitors, stack = []) {
76
+ if (!node || typeof node.type !== "string") {
77
+ return;
78
+ }
79
+
80
+ const visitor = visitors[node.type];
81
+ if (visitor) {
82
+ visitor(node, stack);
83
+ }
84
+
85
+ for (const key in node) {
86
+ if (!node.hasOwnProperty(key)) continue;
87
+
88
+ const child = node[key];
89
+
90
+ if (Array.isArray(child)) {
91
+ child.forEach((c) => {
92
+ if (c) {
93
+ if (typeof c.type === "string") {
94
+ traverseNode(c, visitors, [node].concat(stack));
95
+ } else if (c?.expression && typeof c.expression.type === "string") {
96
+ traverseNode(c.expression, visitors, [node].concat(stack));
97
+ } else if (c?.callee && typeof c.callee.type === "string") {
98
+ traverseNode(c.callee, visitors, [node].concat(stack));
99
+ } else if (c?.left && typeof c.left.type === "string") {
100
+ traverseNode(c.left, visitors, [node].concat(stack));
101
+ } else if (c?.right && typeof c.right.type === "string") {
102
+ traverseNode(c.right, visitors, [node].concat(stack));
103
+ }
104
+ }
105
+ });
106
+ } else if (child && typeof child.type === "string") {
107
+ traverseNode(child, visitors, [node].concat(stack));
108
+ }
109
+ }
110
+ }
111
+
112
+ export function getDeclarations() {
113
+ return DECLARATIONS;
114
+ }
package/lib/styles.js ADDED
@@ -0,0 +1,61 @@
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.push(postcss.parse(content, { parser: safeParser }));
10
+ }
11
+ export function getStylesByClassName(selector) {
12
+ const decls = [];
13
+ for (let root of STYLES) {
14
+ root.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
+ }
package/package.json CHANGED
@@ -1,18 +1,35 @@
1
1
  {
2
2
  "name": "forgecss",
3
- "version": "0.0.1",
3
+ "version": "0.1.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
-
9
+ "build": "node ./scripts/build.js"
10
10
  },
11
- "author": "Krasimir Tsonev",
12
11
  "repository": {
13
12
  "type": "git",
14
13
  "url": "git@github.com:krasimir/ForgeCSS.git"
15
14
  },
16
15
  "license": "MIT",
17
- "keywords": ["css", "dsl", "generator", "processor", "responsive", "media queries"]
16
+ "keywords": [
17
+ "css",
18
+ "dsl",
19
+ "generator",
20
+ "processor",
21
+ "responsive",
22
+ "media queries"
23
+ ],
24
+ "dependencies": {
25
+ "@swc/cli": "^0.7.7",
26
+ "@swc/core": "1.12.1",
27
+ "hast-util-from-html": "^2.0.3",
28
+ "postcss": "^8.5.6",
29
+ "postcss-safe-parser": "^7.0.1",
30
+ "unist-util-visit": "^5.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "esbuild": "^0.27.1"
34
+ }
18
35
  }
@@ -0,0 +1,25 @@
1
+ import path from "path";
2
+ import esbuild from "esbuild";
3
+ import { fileURLToPath } from "url";
4
+ import { readFileSync } from "fs";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
10
+ const minify = true;
11
+
12
+ (async function () {
13
+ await esbuild.build({
14
+ entryPoints: [path.join(__dirname, "..", "client", "index.js")],
15
+ bundle: true,
16
+ minify,
17
+ outfile: path.join(__dirname, "..", "dist", "forgecss.min.js"),
18
+ platform: "browser",
19
+ sourcemap: false,
20
+ plugins: [],
21
+ define: {
22
+ __VERSION__: JSON.stringify(pkg.version)
23
+ }
24
+ });
25
+ })();