@melcanz85/chaincss 1.7.3 → 1.9.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.
@@ -0,0 +1,275 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ class AtomicOptimizer {
5
+ constructor(options = {}) {
6
+ this.options = {
7
+ enabled: true,
8
+ threshold: 3,
9
+ naming: 'hash',
10
+ cache: true,
11
+ cachePath: './.chaincss-cache',
12
+ minify: true,
13
+ ...options
14
+ };
15
+
16
+ this.usageCount = new Map();
17
+ this.atomicClasses = new Map();
18
+ this.stats = {
19
+ totalStyles: 0,
20
+ atomicStyles: 0,
21
+ standardStyles: 0,
22
+ uniqueProperties: 0
23
+ };
24
+
25
+ this.cache = null;
26
+ if (this.options.cache) {
27
+ this.loadCache();
28
+ }
29
+ }
30
+
31
+ loadCache() {
32
+ try {
33
+ if (fs.existsSync(this.options.cachePath)) {
34
+ const data = fs.readFileSync(this.options.cachePath, 'utf8');
35
+ const cache = JSON.parse(data);
36
+
37
+ // Check version compatibility
38
+ if (cache.version === '1.0.0') {
39
+ this.atomicClasses = new Map(cache.atomicClasses || []);
40
+ this.stats = cache.stats || this.stats;
41
+
42
+ const cacheTime = new Date(cache.timestamp).toLocaleString();
43
+ console.log(`Loaded ${this.atomicClasses.size} atomic classes from cache (${cacheTime})`);
44
+
45
+ // Verify config matches
46
+ if (cache.config) {
47
+ if (cache.config.threshold !== this.options.threshold) {
48
+ console.log(`Cache threshold (${cache.config.threshold}) differs from current (${this.options.threshold})`);
49
+ }
50
+ }
51
+ } else {
52
+ console.log('Cache version mismatch, creating new cache');
53
+ }
54
+ }
55
+ } catch (err) {
56
+ console.log('Could not load cache:', err.message);
57
+ }
58
+ }
59
+
60
+ saveCache() {
61
+ try {
62
+ const cache = {
63
+ version: '1.0.0',
64
+ timestamp: Date.now(),
65
+ atomicClasses: Array.from(this.atomicClasses.entries()),
66
+ stats: this.stats
67
+ };
68
+
69
+ // Keep only last 5 cache files for rollback
70
+ const cacheDir = path.dirname(this.options.cachePath);
71
+ if (fs.existsSync(cacheDir)) {
72
+ const files = fs.readdirSync(cacheDir)
73
+ .filter(f => f.startsWith('.chaincss-cache'))
74
+ .map(f => ({
75
+ name: f,
76
+ time: fs.statSync(path.join(cacheDir, f)).mtime.getTime()
77
+ }))
78
+ .sort((a, b) => b.time - a.time);
79
+
80
+ files.slice(4).forEach(f => {
81
+ fs.unlinkSync(path.join(cacheDir, f.name));
82
+ });
83
+ }
84
+
85
+ fs.writeFileSync(this.options.cachePath, JSON.stringify(cache, null, 2), 'utf8');
86
+
87
+ } catch (err) {
88
+ console.log('Could not save cache:', err.message);
89
+ }
90
+ }
91
+
92
+ trackStyles(styles) {
93
+ Object.values(styles).forEach(style => {
94
+ Object.entries(style).forEach(([prop, value]) => {
95
+ if (prop === 'selectors') return;
96
+ const key = `${prop}:${value}`;
97
+ this.usageCount.set(key, (this.usageCount.get(key) || 0) + 1);
98
+ this.stats.totalStyles++;
99
+ });
100
+ });
101
+ this.stats.uniqueProperties = this.usageCount.size;
102
+ }
103
+
104
+ shouldBeAtomic(prop, value) {
105
+ const key = `${prop}:${value}`;
106
+ const usage = this.usageCount.get(key) || 0;
107
+
108
+ // Don't atomic-ify important layout properties that affect order
109
+ const criticalProps = ['position', 'display', 'flex', 'grid', 'z-index'];
110
+ const isCritical = criticalProps.some(p => prop.includes(p));
111
+
112
+ if (isCritical && usage < this.options.threshold * 2) {
113
+ return false; // Keep critical styles in place
114
+ }
115
+
116
+ return usage >= this.options.threshold;
117
+ }
118
+
119
+ generateClassName(prop, value) {
120
+ const key = `${prop}:${value}`;
121
+
122
+ if (this.options.naming === 'hash') {
123
+ let hash = 0;
124
+ for (let i = 0; i < key.length; i++) {
125
+ hash = ((hash << 5) - hash) + key.charCodeAt(i);
126
+ hash |= 0;
127
+ }
128
+ return `_${Math.abs(hash).toString(36).substring(0, 6)}`;
129
+ }
130
+
131
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
132
+ const safeValue = value.replace(/[^a-zA-Z0-9-]/g, '-');
133
+ return `${kebabProp}-${safeValue}`;
134
+ }
135
+
136
+ getOrCreateAtomic(prop, value) {
137
+ const key = `${prop}:${value}`;
138
+
139
+ if (!this.atomicClasses.has(key)) {
140
+ const className = this.generateClassName(prop, value);
141
+ this.atomicClasses.set(key, {
142
+ className,
143
+ prop,
144
+ value,
145
+ usageCount: this.usageCount.get(key) || 0
146
+ });
147
+ this.stats.atomicStyles++;
148
+ }
149
+
150
+ return this.atomicClasses.get(key).className;
151
+ }
152
+
153
+ findKeyByClassName(className) {
154
+ for (let [key, value] of this.atomicClasses.entries()) {
155
+ if (value.className === className) return key;
156
+ }
157
+ return null;
158
+ }
159
+
160
+ generateAtomicCSS() {
161
+ let css = '';
162
+
163
+ const sortedClasses = Array.from(this.atomicClasses.values())
164
+ .sort((a, b) => b.usageCount - a.usageCount);
165
+
166
+ sortedClasses.forEach(atomic => {
167
+ const kebabProp = atomic.prop.replace(/([A-Z])/g, '-$1').toLowerCase();
168
+ css += `.${atomic.className} { ${kebabProp}: ${atomic.value}; }\n`;
169
+ });
170
+
171
+ return css;
172
+ }
173
+
174
+ generateComponentCSS(componentName, style, selectors) {
175
+ const atomicClasses = [];
176
+ const standardStyles = {};
177
+
178
+ // Separate atomic and standard styles
179
+ Object.entries(style).forEach(([prop, value]) => {
180
+ if (prop === 'selectors') return;
181
+
182
+ if (this.shouldBeAtomic(prop, value)) {
183
+ const className = this.getOrCreateAtomic(prop, value);
184
+ atomicClasses.push(className);
185
+ } else {
186
+ standardStyles[prop] = value;
187
+ }
188
+ });
189
+
190
+ let componentCSS = '';
191
+
192
+ if (atomicClasses.length > 0 || Object.keys(standardStyles).length > 0) {
193
+ componentCSS += `${selectors.join(', ')} {\n`;
194
+
195
+ // EXPAND atomic classes into actual properties
196
+ atomicClasses.forEach(className => {
197
+ const key = this.findKeyByClassName(className);
198
+ if (key) {
199
+ const atomic = this.atomicClasses.get(key);
200
+ const kebabProp = atomic.prop.replace(/([A-Z])/g, '-$1').toLowerCase();
201
+ componentCSS += ` ${kebabProp}: ${atomic.value};\n`;
202
+ }
203
+ });
204
+
205
+ // Add standard styles (these will override atomic if needed)
206
+ Object.entries(standardStyles).forEach(([prop, value]) => {
207
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
208
+ componentCSS += ` ${kebabProp}: ${value};\n`;
209
+ });
210
+
211
+ componentCSS += `}\n`;
212
+ }
213
+
214
+ return componentCSS;
215
+ }
216
+
217
+ validateStyleOrder(originalStyles, atomicStyles) {
218
+ // Compare original vs atomic to ensure no missing styles
219
+ const originalProps = new Set();
220
+ const atomicProps = new Set();
221
+
222
+ Object.values(originalStyles).forEach(style => {
223
+ Object.keys(style).forEach(prop => {
224
+ if (prop !== 'selectors') originalProps.add(prop);
225
+ });
226
+ });
227
+
228
+ // Check atomic classes for missing props
229
+ this.atomicClasses.forEach(atomic => {
230
+ atomicProps.add(atomic.prop);
231
+ });
232
+
233
+ const missingProps = [...originalProps].filter(p => !atomicProps.has(p));
234
+ if (missingProps.length > 0) {
235
+ console.warn('Missing atomic classes for:', missingProps);
236
+ }
237
+ }
238
+
239
+ optimize(styles) {
240
+ console.log('ChainCSS Atomic Optimizer running...');
241
+
242
+ // Track usage first
243
+ this.trackStyles(styles);
244
+
245
+ // Generate atomic CSS
246
+ let atomicCSS = this.generateAtomicCSS();
247
+
248
+ // Generate component CSS with expanded styles
249
+ let componentCSS = '';
250
+
251
+ // Process components in original order to maintain specificity
252
+ Object.entries(styles).forEach(([name, style]) => {
253
+ const selectors = style.selectors || [`.${name}`];
254
+ componentCSS += this.generateComponentCSS(name, style, selectors);
255
+ });
256
+
257
+ // Calculate savings
258
+ const savings = ((this.stats.totalStyles - this.atomicClasses.size) / this.stats.totalStyles * 100).toFixed(1);
259
+
260
+ //console.log(`Optimization complete:`);
261
+ //console.log(`Atomic classes created: ${this.atomicClasses.size}`);
262
+ //console.log(`CSS size reduction: ~${savings}%`);
263
+
264
+ // Save cache if enabled
265
+ if (this.options.cache) {
266
+ this.saveCache();
267
+ }
268
+
269
+ // Return atomic CSS first, then component CSS
270
+ // This ensures atomic classes are defined before they're used
271
+ return atomicCSS + componentCSS;
272
+ }
273
+ }
274
+
275
+ module.exports = { AtomicOptimizer };
@@ -0,0 +1,68 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class CacheManager {
5
+ constructor(cachePath = './.chaincss-cache') {
6
+ this.cachePath = path.resolve(process.cwd(), cachePath);
7
+ this.cacheDir = path.dirname(this.cachePath);
8
+ this.cache = {};
9
+ this.load();
10
+ }
11
+
12
+ // Load cache from disk
13
+ load() {
14
+ try {
15
+ if (fs.existsSync(this.cachePath)) {
16
+ const data = fs.readFileSync(this.cachePath, 'utf8');
17
+ this.cache = JSON.parse(data);
18
+ //console.log(`Loaded cache from ${this.cachePath}`);
19
+ } else {
20
+ // Ensure cache directory exists
21
+ if (!fs.existsSync(this.cacheDir)) {
22
+ fs.mkdirSync(this.cacheDir, { recursive: true });
23
+ }
24
+ this.cache = {
25
+ version: '1.0',
26
+ created: new Date().toISOString(),
27
+ atomic: {},
28
+ usage: {}
29
+ };
30
+ }
31
+ } catch (error) {
32
+ console.warn('Could not load cache, starting fresh:', error.message);
33
+ this.cache = {};
34
+ }
35
+ }
36
+
37
+ // Get value from cache
38
+ get(key) {
39
+ return this.cache[key];
40
+ }
41
+
42
+ // Set value in cache
43
+ set(key, value) {
44
+ this.cache[key] = value;
45
+ }
46
+
47
+ // Save cache to disk
48
+ save() {
49
+ try {
50
+ const data = JSON.stringify(this.cache, null, 2);
51
+ fs.writeFileSync(this.cachePath, data, 'utf8');
52
+ //console.log(`Saved cache to ${this.cachePath}`);
53
+ } catch (error) {
54
+ console.warn('Could not save cache:', error.message);
55
+ }
56
+ }
57
+
58
+ // Clear cache
59
+ clear() {
60
+ this.cache = {};
61
+ if (fs.existsSync(this.cachePath)) {
62
+ fs.unlinkSync(this.cachePath);
63
+ }
64
+ console.log('Cache cleared');
65
+ }
66
+ }
67
+
68
+ module.exports = { CacheManager };
package/chaincss.js CHANGED
@@ -1,30 +1,190 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const vm = require('vm');
3
+ const {NodeVM} = require('vm2');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const chokidar = require('chokidar');
7
7
  const CleanCSS = require('clean-css');
8
- const transpilerModule = require('./transpiler.js');
9
8
  const ChainCSSPrefixer = require('./prefixer.js');
9
+ const fileCache = new Map();
10
10
 
11
11
  let prefixerConfig = {
12
12
  enabled: true,
13
13
  browsers: ['> 0.5%', 'last 2 versions', 'not dead'],
14
14
  mode: 'auto' // 'auto', 'lightweight', or 'full'
15
15
  };
16
-
17
16
  const prefixer = new ChainCSSPrefixer(prefixerConfig);
18
17
 
19
- const processScript = (scriptBlock) => {
20
- const context = vm.createContext({ ...transpilerModule});
18
+ // IMPORT THE CORE FROM TRANSPILER - use aliasing
19
+ const { $, run, compile: originalCompile, chain } = require('./transpiler');
20
+
21
+ // Import atomic optimizer
22
+ const { AtomicOptimizer } = require('./atomic-optimizer');
23
+ const { CacheManager } = require('./cache-manager');
24
+
25
+ // Atomic optimizer instance
26
+ let atomicOptimizer = null;
27
+
28
+ // Configuration
29
+ let config = {
30
+ atomic: {
31
+ enabled: false, // Default off for backward compatibility
32
+ threshold: 3,
33
+ naming: 'hash',
34
+ cache: true,
35
+ minify: true
36
+ }
37
+ };
38
+
39
+ try {
40
+ // Try .cjs first (for ES Module projects)
41
+ let configPath = path.join(process.cwd(), 'chaincss.config.cjs');
42
+
43
+ if (fs.existsSync(configPath)) {
44
+ const userConfig = require(configPath);
45
+ config = { ...config, ...userConfig };
46
+ } else {
47
+ // Fall back to .js
48
+ configPath = path.join(process.cwd(), 'chaincss.config.js');
49
+
50
+ if (fs.existsSync(configPath)) {
51
+ const userConfig = require(configPath);
52
+ config = { ...config, ...userConfig };
53
+ } else {
54
+ console.log('No config file found');
55
+ }
56
+ }
57
+
58
+ } catch (err) {
59
+ console.log('Error loading config:', err.message);
60
+ }
61
+
62
+ // Initialize atomic optimizer if enabled
63
+ if (config.atomic.enabled) {
64
+ atomicOptimizer = new AtomicOptimizer(config.atomic);
65
+ } else {
66
+ console.log('Atomic optimizer disabled (config.atomic.enabled =', config.atomic.enabled, ')');
67
+ }
68
+
69
+
70
+ // Create the wrapped compile function
71
+ const compile = (obj) => {
72
+ // First, do standard compilation to get styles
73
+ originalCompile(obj);
74
+
75
+ // If atomic is enabled, optimize
76
+ if (atomicOptimizer && config.atomic.enabled) {
77
+ const optimized = atomicOptimizer.optimize(obj);
78
+ chain.cssOutput = optimized;
79
+ return optimized;
80
+ }
81
+ return chain.cssOutput;
82
+ };
83
+
84
+ // Create a combined module for VM sandbox
85
+ const transpilerModule = {
86
+ $,
87
+ run,
88
+ compile,
89
+ chain
90
+ };
91
+
92
+
93
+ // Recursive file processing function
94
+ const processJCSSFile = (filePath) => {
95
+ // Check cache first
96
+ if (fileCache.has(filePath)) {
97
+ return fileCache.get(filePath);
98
+ }
99
+
100
+ // Check if file exists
101
+ if (!fs.existsSync(filePath)) {
102
+ throw new Error(`File not found: ${filePath}`);
103
+ }
104
+
105
+ const content = fs.readFileSync(filePath, 'utf8');
106
+
107
+ // Create a new VM instance for this file
108
+ const vm = new NodeVM({
109
+ console: 'inherit',
110
+ timeout: 5000,
111
+ sandbox: {
112
+ ...transpilerModule,
113
+ get: (relativePath) => {
114
+ const baseDir = path.dirname(filePath);
115
+ const targetPath = path.resolve(baseDir, relativePath);
116
+ return processJCSSFile(targetPath);
117
+ },
118
+ __filename: filePath,
119
+ __dirname: path.dirname(filePath),
120
+ module: { exports: {} },
121
+ exports: {}
122
+ },
123
+ require: {
124
+ external: true,
125
+ builtin: ['path', 'fs'],
126
+ root: './'
127
+ }
128
+ });
129
+
130
+ // Wrap the content - DON'T redeclare module!
131
+ const wrappedContent = `
132
+ // Clear any existing exports
133
+ module.exports = {};
134
+
135
+ // Run the actual file content
136
+ ${content}
137
+
138
+ // Return the exports
139
+ module.exports;
140
+ `;
141
+
142
+ try {
143
+ const exports = vm.run(wrappedContent, filePath);
144
+ fileCache.set(filePath, exports);
145
+ return exports;
146
+ } catch (err) {
147
+ console.error(`Error processing ${filePath}:`, err.message);
148
+ throw err;
149
+ }
150
+ };
151
+
152
+ const processScript = (scriptBlock,filename) => {
153
+
154
+ const vm = new NodeVM({
155
+ console: 'inherit',
156
+ timeout: 5000,
157
+ sandbox: {
158
+ ...transpilerModule,
159
+ get: (relativePath) => {
160
+ const baseDir = path.dirname(filename);
161
+ const targetPath = path.resolve(baseDir, relativePath);
162
+ return processJCSSFile(targetPath);
163
+ },
164
+ __filename: filename,
165
+ __dirname: path.dirname(filename),
166
+ module: { exports: {} },
167
+ require: (path) => require(path)
168
+ },
169
+ require: {
170
+ external: true, // Allow some external modules
171
+ builtin: ['path', 'fs'], // Allow specific Node built-ins
172
+ root: './' // Restrict to project root
173
+ }
174
+ });
175
+
21
176
  const jsCode = scriptBlock.trim();
22
- const chainScript = new vm.Script(jsCode);
23
- chainScript.runInContext(context);
24
- return context.chain.cssOutput;
177
+
178
+ try {
179
+ const result = vm.run(jsCode, filename);
180
+ return transpilerModule.chain.cssOutput;
181
+ } catch (err) {
182
+ console.error(`Error processing script in ${filename}:`, err.message);
183
+ throw err;
184
+ }
25
185
  };
26
186
 
27
- const processJavascriptBlocks = (content) => {
187
+ const processJavascriptBlocks = (content, inputpath) => {
28
188
  const blocks = content.split(/<@([\s\S]*?)@>/gm);
29
189
  let outputCSS = '';
30
190
  for (let i = 0; i < blocks.length; i++) {
@@ -33,7 +193,8 @@ const processJavascriptBlocks = (content) => {
33
193
  } else {
34
194
  const scriptBlock = blocks[i];
35
195
  try {
36
- const outputProcessScript = processScript(scriptBlock);
196
+ const outputProcessScript = processScript(scriptBlock,inputpath);
197
+
37
198
  if (typeof outputProcessScript !== 'object' && typeof outputProcessScript !== 'undefined') {
38
199
  outputCSS += outputProcessScript;
39
200
  }
@@ -103,8 +264,7 @@ const processor = async (inputFile, outputFile) => {
103
264
  const content = fs.readFileSync(input, 'utf8');
104
265
 
105
266
  // STEP 1: Process JavaScript blocks first
106
- const processedCSS = processJavascriptBlocks(content);
107
-
267
+ const processedCSS = processJavascriptBlocks(content, input);
108
268
  // STEP 2: Validate the CSS
109
269
  if (!validateCSS(processedCSS)) {
110
270
  throw new Error('Invalid CSS syntax');
@@ -155,8 +315,7 @@ function parseArgs(args) {
155
315
  noPrefix: false,
156
316
  browsers: null,
157
317
  prefixerMode: 'auto',
158
- // NEW: Add these
159
- sourceMap: true, // Enable source maps by default
318
+ sourceMap: true,
160
319
  sourceMapInline: false
161
320
  };
162
321
 
@@ -173,7 +332,6 @@ function parseArgs(args) {
173
332
  } else if (arg === '--browsers' && args[i + 1]) {
174
333
  result.browsers = args[i + 1].split(',');
175
334
  i++;
176
- // NEW: Add these two
177
335
  } else if (arg === '--no-source-map') {
178
336
  result.sourceMap = false;
179
337
  } else if (arg === '--source-map-inline') {
@@ -253,4 +411,13 @@ Examples:
253
411
  })();
254
412
  }
255
413
 
256
- module.exports = { processor, watch };
414
+ module.exports = {
415
+ processor,
416
+ watch,
417
+ $,
418
+ run,
419
+ compile,
420
+ chain,
421
+ atomicOptimizer,
422
+ config
423
+ };
package/index.js ADDED
@@ -0,0 +1,24 @@
1
+ const { $, run, compile, chain, tokens, createTokens } = require('./transpiler');
2
+ const { processor, watch } = require('./chaincss');
3
+
4
+ // Conditionally export React hooks if in React environment
5
+ let reactHooks = {};
6
+ try {
7
+ // Check if React is available
8
+ require.resolve('react');
9
+ reactHooks = require('./react-hooks');
10
+ } catch {
11
+
12
+ }
13
+
14
+ module.exports = {
15
+ $,
16
+ run,
17
+ compile,
18
+ processor,
19
+ watch,
20
+ chain,
21
+ tokens,
22
+ createTokens,
23
+ ...reactHooks
24
+ };
package/index.react.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './react-hooks';
2
+
3
+ // Re-export core for convenience
4
+ export { $, tokens, createTokens } from './transpiler';
package/package.json CHANGED
@@ -1,8 +1,31 @@
1
1
  {
2
2
  "name": "@melcanz85/chaincss",
3
- "version": "1.7.3",
3
+ "version": "1.9.0",
4
4
  "description": "A simple package transpiler for js to css",
5
- "main": "chaincss.js",
5
+ "main": "index.js",
6
+ "module": "index.react.js",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./index.js",
10
+ "import": "./index.react.js"
11
+ },
12
+ "./react": {
13
+ "import": "./index.react.js"
14
+ }
15
+ },
16
+ "types": "types.d.ts",
17
+ "files": [
18
+ "index.js",
19
+ "index.react.js",
20
+ "chaincss.js",
21
+ "transpiler.js",
22
+ "tokens.js",
23
+ "react-hooks.js",
24
+ "atomic-optimizer.js",
25
+ "cache-manager.js",
26
+ "prefixer.js",
27
+ "types.d.ts"
28
+ ],
6
29
  "publishConfig": {
7
30
  "access": "public",
8
31
  "registry": "https://registry.npmjs.org/"
package/prefixer.js CHANGED
@@ -89,7 +89,7 @@ class ChainCSSPrefixer {
89
89
  // Set up source map options
90
90
  const mapOptions = {
91
91
  inline: this.config.sourceMapInline,
92
- annotation: false, // We'll add the comment ourselves
92
+ annotation: false,
93
93
  sourcesContent: true
94
94
  };
95
95
 
@@ -100,7 +100,7 @@ class ChainCSSPrefixer {
100
100
  return await this.processWithBuiltIn(cssString, options, mapOptions);
101
101
 
102
102
  } catch (err) {
103
- console.error('⚠️ Prefixer error:', err.message);
103
+ console.error('Prefixer error:', err.message);
104
104
  return { css: cssString, map: null };
105
105
  }
106
106
  }