forgecss 0.1.7 → 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 CHANGED
@@ -13,7 +13,7 @@ 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.`);
@@ -30,11 +30,15 @@ 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
- if (!config.source) {
35
- throw new Error('forgecss: missing "source" in configuration.');
36
- }
37
- const watcher = chokidar.watch(config.source, {
41
+ const watcher = chokidar.watch(config.dir, {
38
42
  persistent: true,
39
43
  ignoreInitial: true,
40
44
  ignored: (p, stats) => path.resolve(p) === path.resolve(config.output)
@@ -50,9 +54,13 @@ async function runForgeCSS(lookAtPath = null) {
50
54
  }
51
55
  }
52
56
  }
53
- await ForgeCSS(config).parse(lookAtPath);
57
+ if (lookAtPath) {
58
+ instance.parseFile(lookAtPath, config.output);
59
+ } else {
60
+ instance.parseDirectory(config.dir, config.output);
61
+ }
54
62
  if (options.verbose) {
55
- console.log(`forgecss: CSS generation at ${config.output} completed.`);
63
+ console.log(`forgecss: ${config.output} generated successfully.`);
56
64
  }
57
65
  }
58
66
 
package/index.d.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  export type ForgeCSSOptions = {
2
- source: string;
3
2
  inventoryFiles?: string[];
4
3
  usageFiles?: string[];
5
4
  usageAttributes?: string[];
6
- mapping: {
7
- queries: {
8
- [key: string]: {
9
- query: string;
10
- };
5
+ mapping?: {
6
+ queries?: {
7
+ [key: string]: string
11
8
  };
12
9
  };
13
- output: string;
14
10
  };
15
11
 
16
12
  export type ForgeInstance = {
17
- parse: (filePathToSpecificFile?: string) => Promise<void>;
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>;
18
16
  };
19
17
 
20
- export default function forgecss(options?: ForgeCSSOptions): ForgeInstance;
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/inventory.js";
3
+ import { extractStyles, invalidateInvetory } from "./lib/inventory.js";
3
4
  import { invalidateUsageCache, findUsages } from "./lib/processor.js";
4
5
  import { generateOutputCSS } from "./lib/generator.js";
5
6
 
6
7
  const DEFAULT_OPTIONS = {
7
- source: null,
8
8
  inventoryFiles: ["css", "less", "scss"],
9
9
  usageFiles: ["html", "jsx", "tsx"],
10
10
  usageAttributes: ["class", "className"],
11
11
  mapping: {
12
12
  queries: {}
13
- },
14
- output: null
13
+ }
15
14
  };
16
15
 
17
- export default function ForgeCSS(options = { source: null, output: null, mapping: {} }) {
16
+ export default function ForgeCSS(options) {
18
17
  const config = { ...DEFAULT_OPTIONS };
19
18
 
20
- config.source = options.source ?? DEFAULT_OPTIONS.source;
21
- config.mapping = Object.assign({}, DEFAULT_OPTIONS.mapping, options.mapping ?? {});
22
- config.output = options.output ?? DEFAULT_OPTIONS.output;
19
+ config.mapping = {
20
+ queries: Object.assign({}, DEFAULT_OPTIONS.mapping.queries, options?.mapping?.queries ?? {})
21
+ };
23
22
 
24
- if (!config.source) {
25
- throw new Error('forgecss: missing "source" in configuration.');
26
- }
27
- if (!config.output) {
28
- throw new Error('forgecss: missing "output" in configuration.');
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 parse(lookAtPath = null) {
37
+ async parseDirectory(dir, output = null) {
38
+ if (!dir) {
39
+ throw new Error('forgecss: parseDirectory requires "dir" as an argument.');
40
+ }
41
+ try {
42
+ // filling the inventory
43
+ let files = await getAllFiles(dir, config.inventoryFiles);
44
+ for (let file of files) {
45
+ await extractStyles(file);
46
+ }
47
+ } catch (err) {
48
+ console.error(`forgecss: error extracting styles.`, err);
49
+ }
50
+ // finding the usages
51
+ try {
52
+ let files = await getAllFiles(dir, config.usageFiles);
53
+ for (let file of files) {
54
+ await findUsages(file);
55
+ }
56
+ } catch (err) {
57
+ console.error(`forgecss: error extracting usages`, err);
58
+ }
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();
33
67
  // filling the inventory
34
68
  try {
35
- if (lookAtPath) {
36
- if (config.inventoryFiles.includes(lookAtPath.split(".").pop().toLowerCase())) {
37
- await extractStyles(lookAtPath);
38
- }
39
- } else {
40
- let files = await getAllFiles(config.source, config.inventoryFiles);
41
- for (let file of files) {
42
- await extractStyles(file);
43
- }
69
+ if (config.inventoryFiles.includes(ext)) {
70
+ await extractStyles(file);
44
71
  }
45
72
  } catch (err) {
46
- console.error(`forgecss: error extracting styles: ${err}`);
73
+ console.error(`forgecss: error extracting styles.`, err);
47
74
  }
48
75
  // finding the usages
49
76
  try {
50
- if (lookAtPath) {
51
- if (config.usageFiles.includes(lookAtPath.split(".").pop().toLowerCase())) {
52
- invalidateUsageCache(lookAtPath);
53
- await findUsages(lookAtPath);
54
- }
55
- } else {
56
- let files = await getAllFiles(config.source, config.usageFiles);
57
- for (let file of files) {
58
- await findUsages(file);
59
- }
77
+ if (config.usageFiles.includes(ext)) {
78
+ invalidateUsageCache(file);
79
+ await findUsages(file);
60
80
  }
61
81
  } catch (err) {
62
- console.error(`forgecss: error extracting declarations: ${err}`);
82
+ console.error(`forgecss: error extracting usages.`, err);
63
83
  }
64
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 generateOutputCSS(config);
104
+ await findUsages("usage.html", html);
67
105
  } catch (err) {
68
- console.error(`forgecss: error generating output CSS: ${err}`);
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,67 +1,37 @@
1
- import postcss from "postcss";
2
- import { writeFile } from "fs/promises";
3
-
4
1
  import { getUsages } from "./processor.js";
5
- import { getStylesByClassName } from "./inventory.js";
2
+ import mediaQueryTransformer from "./transformers/mediaQuery.js";
3
+ import pseudoClassTransformer from "./transformers/pseudo.js";
6
4
 
7
5
  export async function generateOutputCSS(config) {
8
- const cache = {};
6
+ const bucket = {};
9
7
  const usages = getUsages();
10
8
  Object.keys(usages).map((file) => {
11
9
  Object.keys(usages[file]).forEach(async (label) => {
12
10
  try {
13
- createMediaStyle(config, label, usages[file][label], cache);
11
+ if (mediaQueryTransformer(config, label, usages[file][label], bucket)) {
12
+ return;
13
+ } else if (pseudoClassTransformer(label, usages[file][label], bucket)) {
14
+ return;
15
+ }
14
16
  } catch (err) {
15
- console.error(`Error generating media query for label ${label} (found in file ${file}): ${err}`);
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
+ );
16
24
  }
17
25
  });
18
26
  });
19
- const result = Object.keys(cache)
20
- .map((label) => cache[label].mq.toString())
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)
21
35
  .join("\n");
22
- await writeFile(
23
- config.output,
24
- `/* ForgeCSS autogenerated file */\n${result}`,
25
- "utf-8"
26
- );
27
36
  }
28
- export function createMediaStyle(config, label, selectors, cache) {
29
- if (!config.mapping.queries[label]) {
30
- throw new Error(
31
- `Unknown media query label: ${label}. Check app-fe/wwwroot/scripts/lib/generateMediaQueries.js for available mappings.`
32
- );
33
- }
34
- if (!cache[label]) {
35
- cache[label] = {
36
- mq: postcss.atRule({
37
- name: "media",
38
- params: `all and (${config.mapping.queries[label].query})`
39
- }),
40
- classes: {}
41
- };
42
- }
43
- const mq = cache[label].mq;
44
- selectors.forEach((selector) => {
45
- const prefixedSelector = `.${label}_${selector}`;
46
- if (cache[label].classes[prefixedSelector]) {
47
- return;
48
- }
49
- cache[label].classes[prefixedSelector] = true;
50
- const rule = postcss.rule({ selector: prefixedSelector });
51
- const decls = getStylesByClassName(selector);
52
- if (decls.length === 0) {
53
- console.warn(`Warning: No styles found for class .${selector} used in media query ${label}`);
54
- return;
55
- }
56
- decls.forEach((d) => {
57
- rule.append(
58
- postcss.decl({
59
- prop: d.prop,
60
- value: d.value,
61
- important: d.important
62
- })
63
- );
64
- });
65
- mq.append(rule);
66
- });
67
- }
37
+
@@ -0,0 +1,3 @@
1
+ export function extractStyles(filePath: string, css?: string | null): Promise<void>;
2
+ export function getStylesByClassName(selector: string): object[];
3
+ export function invalidateInvetory(): void;
package/lib/inventory.js CHANGED
@@ -2,10 +2,10 @@ import { readFile } from "fs/promises";
2
2
  import postcss from "postcss";
3
3
  import safeParser from "postcss-safe-parser";
4
4
 
5
- const INVENTORY = {};
5
+ let INVENTORY = {};
6
6
 
7
- export async function extractStyles(filePath) {
8
- const content = await readFile(filePath, 'utf-8');
7
+ export async function extractStyles(filePath, css = null) {
8
+ const content = css !== null ? css : await readFile(filePath, 'utf-8');
9
9
  INVENTORY[filePath] = postcss.parse(content, { parser: safeParser });
10
10
  }
11
11
  export function getStylesByClassName(selector) {
@@ -20,4 +20,7 @@ export function getStylesByClassName(selector) {
20
20
  });
21
21
  });
22
22
  return decls;
23
+ }
24
+ export function invalidateInvetory() {
25
+ INVENTORY = {};
23
26
  }
@@ -0,0 +1,3 @@
1
+ export declare function findUsages(filePath: string, content?: string | null): Promise<void>;
2
+ export declare function invalidateUsageCache(filePath?: string): void;
3
+ export declare function getUsages(): object;
package/lib/processor.js CHANGED
@@ -4,18 +4,18 @@ import { fromHtml } from "hast-util-from-html";
4
4
  import { visit } from "unist-util-visit";
5
5
 
6
6
  const FUNC_NAME = 'fx';
7
- const USAGES = {};
7
+ let USAGES = {};
8
8
 
9
9
  const { parse } = swc;
10
10
 
11
- export async function findUsages(filePath) {
11
+ export async function findUsages(filePath, fileContent = null) {
12
12
  try {
13
13
  if (USAGES[filePath]) {
14
14
  // already processed
15
15
  return;
16
16
  }
17
17
  USAGES[filePath] = {};
18
- const content = await readFile(filePath, "utf-8");
18
+ const content = fileContent ? fileContent : await readFile(filePath, "utf-8");
19
19
  const extension = filePath.split('.').pop().toLowerCase();
20
20
 
21
21
  // HTML
@@ -23,7 +23,7 @@ export async function findUsages(filePath) {
23
23
  const ast = fromHtml(content);
24
24
  visit(ast, "element", (node) => {
25
25
  if (node.properties.className) {
26
- pushToDeclarations(filePath, node.properties.className.join(' '));
26
+ storeUsage(filePath, node.properties.className.join(' '));
27
27
  }
28
28
  });
29
29
  return;
@@ -47,21 +47,28 @@ export async function findUsages(filePath) {
47
47
  value += elem?.cooked || "";
48
48
  });
49
49
  }
50
- pushToDeclarations(filePath, value);
50
+ storeUsage(filePath, value);
51
51
  }
52
52
  }
53
53
  }
54
54
  });
55
55
  } catch (err) {
56
- console.error(`forgecss: error processing file ${filePath}: ${err}`);
56
+ console.error(`forgecss: error processing file ${filePath.replace(process.cwd(), '')}`, err);
57
57
  }
58
58
  }
59
59
  export function invalidateUsageCache(filePath) {
60
+ if (!filePath) {
61
+ USAGES = {};
62
+ return;
63
+ }
60
64
  if (USAGES[filePath]) {
61
65
  delete USAGES[filePath];
62
66
  }
63
67
  }
64
- function pushToDeclarations(filePath, classesString = "") {
68
+ export function getUsages() {
69
+ return USAGES;
70
+ }
71
+ function storeUsage(filePath, classesString = "") {
65
72
  if (classesString) {
66
73
  classesString.split(" ").forEach((part) => {
67
74
  if (part.indexOf(":") > -1) {
@@ -112,7 +119,4 @@ function traverseASTNode(node, visitors, stack = []) {
112
119
  traverseASTNode(child, visitors, [node].concat(stack));
113
120
  }
114
121
  }
115
- }
116
- export function getUsages() {
117
- return USAGES;
118
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.1.7",
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
@@ -0,0 +1,3 @@
1
+ {
2
+ "include": ["lib/**/*", "index.d.ts", "**/*.js"],
3
+ }