forgecss 0.0.1 → 0.1.1

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 ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { program } from "commander";
7
+ import ForgeCSS from './index.js';
8
+ import chokidar from "chokidar";
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);
13
+ program.parse();
14
+
15
+ const options = program.opts();
16
+ let config = null;
17
+
18
+ if (!fs.existsSync(options.config)) {
19
+ throw new Error(`forgecss: Config file not found at ${options.config}. Check the --config option.`);
20
+ }
21
+
22
+ async function loadConfig(configPath) {
23
+ const abs = path.resolve(configPath);
24
+ const fileUrl = pathToFileURL(abs).href;
25
+
26
+ const mod = await import(fileUrl);
27
+ return mod.default ?? mod; // support both default and named export
28
+ }
29
+ async function runForgeCSS() {
30
+ if (!config) {
31
+ config = await loadConfig(options.config);
32
+ if (options.watch) {
33
+ const watcher = chokidar.watch([
34
+ config.styles.sourceDir,
35
+ config.ui.sourceDir
36
+ ], {
37
+ persistent: true,
38
+ ignoreInitial: true,
39
+ ignored: (p, stats) => {
40
+ if (path.resolve(p) === path.resolve(config.output)){
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ });
46
+ watcher.on("change", async (filePath) => {
47
+ if (options.verbose) {
48
+ console.log(`forgecss: Detected change in ${filePath}`);
49
+ }
50
+ runForgeCSS();
51
+ });
52
+ if (options.verbose) {
53
+ console.log("forgecss: Watch mode enabled. Listening for file changes...");
54
+ }
55
+ }
56
+ }
57
+ ForgeCSS(config).parse();
58
+ }
59
+
60
+ runForgeCSS();
61
+
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
+ import { getDeclarations } from "./processFile.js";
3
+ import { createMediaStyle } from "./styles.js";
4
+
5
+ export async function generateOutputCSS(config) {
6
+ const cache = {};
7
+ const declarations = getDeclarations();
8
+ console.log(declarations);
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
+ `/* ForgeCSS autogenerated file */\n${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 = 'fx';
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,40 @@
1
1
  {
2
2
  "name": "forgecss",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
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
+ "chokidar": "^4.0.3",
28
+ "commander": "^14.0.2",
29
+ "hast-util-from-html": "^2.0.3",
30
+ "postcss": "^8.5.6",
31
+ "postcss-safe-parser": "^7.0.1",
32
+ "unist-util-visit": "^5.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "esbuild": "^0.27.1"
36
+ },
37
+ "bin": {
38
+ "forgecss": "./cli.js"
39
+ }
18
40
  }
@@ -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
+ })();