@melcanz85/chaincss 1.5.10 → 1.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.
@@ -0,0 +1,22 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '24'
20
+ registry-url: 'https://registry.npmjs.org'
21
+ - run: npm ci
22
+ - run: npm publish
package/README.md CHANGED
@@ -8,18 +8,19 @@ A simple JavaScript-to-CSS transpiler that converts JS objects into optimized CS
8
8
  ## 🚀 Installation
9
9
 
10
10
  ```bash
11
- npm install @melcanz85/chaincss
12
-
11
+ npm install @melcanz85/chaincss
12
+ ```
13
13
  📦 Usage (Node.js)
14
- Quick Setup
15
14
 
16
- Install development dependencies:
15
+ Quick Setup
17
16
 
18
- bash
17
+ ### Install development dependencies:
19
18
 
20
- npm install --save-dev nodemon concurrently
19
+ ```bash
20
+ npm install --save-dev nodemon concurrently
21
+ ```
21
22
 
22
- Update your package.json scripts:
23
+ ### Update your package.json scripts:
23
24
 
24
25
  json
25
26
 
@@ -27,6 +28,27 @@ json
27
28
  "start": "concurrently \"nodemon server.js\" \"nodemon --watch chaincss/*.jcss --watch processor.js --exec 'node processor.js'\""
28
29
  }
29
30
 
31
+
32
+ ## 🔧 CSS Prefixing
33
+
34
+ ChainCSS offers two prefixing modes:
35
+
36
+ ### 1. Lightweight Mode (Default, ~50KB)
37
+ Built-in prefixer that handles the most common CSS properties:
38
+ - Flexbox & Grid
39
+ - Transforms & Animations
40
+ - Filters & Effects
41
+ - Text effects
42
+ - Box properties
43
+
44
+ No additional installation needed!
45
+
46
+ ### 2. Full Mode (Uses Autoprefixer)
47
+ For complete prefixing coverage of all CSS properties:
48
+
49
+ ```bash
50
+ npm install autoprefixer postcss browserslist caniuse-db
51
+ ```
30
52
  Project Structure
31
53
 
32
54
  Create this folder structure in your project:
@@ -48,93 +70,118 @@ The Initialization processor Setup
48
70
 
49
71
  In chaincss/processor.js:
50
72
 
51
- const chain = require("@melcanz85/chaincss");
73
+ const chain = require("@melcanz85/chaincss");
52
74
 
53
- try {
54
- // Process main file and output CSS
55
- chain.processor('./chaincss/main.jcss', './public/style.css');
56
- } catch (err) {
57
- console.error('Error processing chainCSS file:', err.stack);
58
- process.exit(1);
59
- }
75
+ try {
76
+ // Process main file and output CSS
77
+ chain.processor('./chaincss/main.jcss', './public/style.css');
78
+ } catch (err) {
79
+ console.error('Error processing chainCSS file:', err.stack);
80
+ process.exit(1);
81
+ }
60
82
 
61
83
  💻 Code Examples
62
84
 
63
- --Chaining File (chaincss/chain.jcss):
64
-
65
- *** This is where the chaining happens all codes here are in javascript syntax, the methods are the css properties but in javascript form it follows the camelCase syntax. Example the property font-family is fontFamily in chaincss and your css selector is the value of the block() method which is always at the end of the chain.
66
-
67
- *** The property method are the same as the css property but background is an exception because it's a long word so it is shorten to bg only. Example background-color is bgColor() in chaincss etc.
68
-
69
- // Variables for consistent styling
70
- const bodyBgColor = '#f0f0f0';
71
- const headerBgColor = '#333';
72
- const bodyFontFamily = 'Arial, sans-serif';
73
- const headerAlignItems = 'center';
74
- const logoHeight = '50px';
75
-
76
- // Reset browser defaults
77
- const resetDefaultBrowStyle = chain
78
- .margin('0')
79
- .padding('0')
80
- .block('body', 'h1', 'h2', 'h3', 'p', 'ul');
81
-
82
- // Body styles
83
- const bodyStyle = chain
84
- .fontFamily(bodyFontFamily)
85
- .lineHeight('1.6')
86
- .bgColor(bodyBgColor)
87
- .block('body');
88
-
89
- // Header styles
90
- const header = chain
91
- .display('flex')
92
- .alignItems(headerAlignItems)
93
- .justifyContent('space-between')
94
- .bgColor(headerBgColor)
95
- .color('#fff')
96
- .padding('10px 20px')
97
- .block('header');
98
-
99
- // Logo
100
- const logoImgHeight = chain
101
- .height(logoHeight)
102
- .block('.logo img');
103
-
104
- module.exports = {
105
- resetDefaultBrowStyle,
106
- bodyStyle,
107
- header,
108
- logoImgHeight
109
- };
110
-
111
-
112
- --Main File (chaincss/main.jcss):
113
-
114
- <@
115
- // Import chaining definitions
116
- const style = get('./chain.jcss');
117
-
118
- // Override specific styles
119
- style.header.bgColor = 'red';
120
-
121
- // Compile to CSS
122
- compile(style);
123
- @>
124
-
125
- @media (max-width: 768px) {
126
- <@
127
- run(
128
- chain.flexDirection('column').alignItems('flex-start').block('header'),
129
- chain.order(1).block('.logo'),
130
- chain.order(2).block('.search-bar'),
131
- chain.order(3).block('h1'),
132
- chain.order(5).block('nav'),
133
- chain.order(4).display('flex').width('100%').justifyContent('flex-end').block('.burgerWrapper')
134
- );
135
- @>
136
- }
85
+ //--Chaining File (chaincss/chain.jcss):
86
+
87
+ ### This is where the chaining happens all codes must be in javascript syntax.
88
+ The chain methods are the same as the css properties but in camelCase mode
89
+ and the exception of the background property which is shorten to 'bg' only for
90
+ example background-color is bgColor() in chaincss. The value of the block()
91
+ method is the css selector which is always at the end of the chain or block.
92
+
93
+ // Variables for consistent styling
94
+ const bodyBg = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
95
+ const headerBg = 'rgba(255, 255, 255, 0.95)';
96
+ const bodyFontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
97
+ Ubuntu, sans-serif";
98
+ const headerBoxShadow = '0 2px 20px rgba(0,0,0,0.1)';
99
+ const logoFontSize = '1.8rem';
100
+
101
+ const reset = chain
102
+ .margin('0')
103
+ .padding('0')
104
+ .boxSizing('border-box')
105
+ .block('*');
106
+
107
+ const body = chain
108
+ .fontFamily(bodyFontFamily)
109
+ .lineHeight('1.6')
110
+ .color('#1e293b')
111
+ .bg(bodyBg)
112
+ .block('body');
113
+
114
+ /* Header/Navigation */
115
+ const navbar = chain
116
+ .bg(headerBg)
117
+ .backdropFilter('blur(10px)')
118
+ .padding('1rem 5%')
119
+ .position('fixed')
120
+ .width('100%')
121
+ .top('0')
122
+ .zIndex('1000')
123
+ .boxShadow(headerBoxShadow)
124
+ .block('.navbar');
125
+
126
+ const nav_container = chain
127
+ .maxWidth('1200px')
128
+ .margin('0 auto')
129
+ .display('flex')
130
+ .justifyContent('space-between')
131
+ .alignItems('center')
132
+ .block('.nav-container');
133
+
134
+ const logo = chain
135
+ .fontSize(logoFontSize)
136
+ .fontWeight('700')
137
+ .bg('linear-gradient(135deg, #667eea, #764ba2)')
138
+ .backgroundClip('text')
139
+ .textFillColor('transparent')
140
+ .letterSpacing('-0.5px')
141
+ .block('.logo');
142
+
143
+ module.exports = {
144
+ reset,
145
+ navbar,
146
+ nav_container,
147
+ logo
148
+ };
149
+
150
+
151
+ //--Main File (chaincss/main.jcss):
152
+
153
+ <@
154
+ // Import chaining definitions
155
+ const style = get('./chain.jcss');
156
+
157
+ // Override specific styles
158
+ style.logo.fontSize = '2rem';
159
+
160
+ // Compile to CSS
161
+ compile(style);
162
+ @>
163
+
164
+ @keyframes fadeInUp {
165
+ <@
166
+ run(
167
+ chain.opacity('0').transform('translateY(20px)').block('from'),
168
+ chain.opacity('1').transform('translateY(0)').block('to'),
169
+ );
170
+ @>
171
+ }
137
172
 
173
+ /* Responsive */
174
+ @media (max-width: 768px) {
175
+ <@
176
+ run(
177
+ chain.fontSize('2.5rem').block('.hero h1'),
178
+ chain.flexDirection('column').gap('1rem').block('.stats'),
179
+ chain.flexDirection('column').alignItems('center').block('.cta-buttons'),
180
+ chain.gridTemplateColumns('1fr').block('.example-container'),
181
+ chain.display('none').block('.nav-links')
182
+ );
183
+ @>
184
+ }
138
185
 
139
186
  📝 Notes
140
187
 
@@ -142,13 +189,16 @@ module.exports = {
142
189
 
143
190
  But chainCSS syntax must be wrapped in <@ @> delimiters.
144
191
 
145
- The get() function imports chaining definitions from other files
192
+ The get() function imports chaining definitions from your chain.jcss file
146
193
 
147
- YOU can modify your style in between get() and compile() in the main file it will overwrite the styles in the chainn file.
194
+ You can modify your style in between get() and compile() in the
195
+ main file it will overwrite the styles in the chain file.
148
196
 
149
197
  🎨 Editor Support
150
198
 
151
- Since .jcss files are just JavaScript files with ChainCSS syntax, you can easily enable proper syntax highlighting in your editor:
199
+ Since .jcss files are just JavaScript files with ChainCSS syntax, you can
200
+ easily enable proper syntax highlighting in your editor:
201
+
152
202
  VS Code
153
203
 
154
204
  Add this to your project's .vscode/settings.json:
@@ -170,13 +220,13 @@ WebStorm / IntelliJ IDEA
170
220
  Vim / Neovim
171
221
 
172
222
  Add to your .vimrc or init.vim:
173
- vim
174
223
 
175
- au BufRead,BufNewFile *.jcss setfiletype javascript
224
+ au BufRead,BufNewFile *.jcss setfiletype javascript
176
225
 
177
226
  Sublime Text
178
227
 
179
- Create or edit ~/Library/Application Support/Sublime Text/Packages/User/JCSS.sublime-settings:
228
+ Create or edit ~/Library/Application Support/Sublime Text/Packages/User/JCSS.sublime-settings:
229
+
180
230
  json
181
231
 
182
232
  {
@@ -189,12 +239,13 @@ Atom
189
239
  Add to your config.cson:
190
240
  coffeescript
191
241
 
192
- "*":
193
- core:
194
- customFileTypes:
195
- "source.js": [
196
- "jcss"
197
- ]
242
+ "*":
243
+ core:
244
+ customFileTypes:
245
+ "source.js": [
246
+ "jcss"
247
+ ]
248
+
198
249
 
199
250
  Other Editors
200
251
 
@@ -207,19 +258,17 @@ Status Feature Description
207
258
 
208
259
  ✅ Basic JS → CSS Convert plain JS objects to CSS
209
260
 
210
- 🚧 Keyframe animations @keyframes support
211
-
212
- 🚧 Vendor prefixing Auto-add -webkit-, -moz-, etc.
213
-
214
- 🚧 Source maps Debug generated CSS
261
+ Vendor prefixing Auto-add -webkit-, -moz-, etc.
215
262
 
216
- 🚧 Watch mode Auto-recompile on file changes
217
-
218
- = Working, 🚧 = Coming soon
263
+ Keyframe animations @keyframes support
264
+
265
+ Source maps Debug generated CSS
219
266
 
267
+ ✅ Watch mode Auto-recompile on file changes
220
268
 
221
269
  👨‍💻 Contributing
222
- Contributions are welcome! Feel free to open issues or submit pull requests.
270
+
271
+ Contributions are welcome! Feel free to open issues or submit pull requests.
223
272
 
224
273
  📄 License
225
274
 
package/chaincss.js CHANGED
@@ -7,38 +7,29 @@ const fs = require('fs');
7
7
  const chokidar = require('chokidar');
8
8
  const CleanCSS = require('clean-css');
9
9
  const transpilerModule = require('./transpiler.js');
10
+ const ChainCSSPrefixer = require('./prefixer.js');
11
+
12
+ let prefixerConfig = {
13
+ enabled: true,
14
+ browsers: ['> 0.5%', 'last 2 versions', 'not dead'],
15
+ mode: 'auto' // 'auto', 'lightweight', or 'full'
16
+ };
17
+
18
+ const prefixer = new ChainCSSPrefixer(prefixerConfig);
10
19
 
11
- // FUNCTION TO PROCESS CHUNKS OF SCRIPTS FROM THE INPUT FILE USING THE VM MODULE
12
20
  const processScript = (scriptBlock) => {
13
21
  const context = vm.createContext({
14
- ...transpilerModule
22
+ ...transpilerModule
15
23
  });
16
- const jsCode = scriptBlock.trim();
24
+
25
+ const jsCode = scriptBlock.trim();
17
26
  const chainScript = new vm.Script(jsCode);
18
27
  chainScript.runInContext(context);
19
- return context.chain.cssOutput;
20
- };
21
-
22
- // CSS Minification Function
23
- const minifyCss = (css) => {
24
- const output = new CleanCSS().minify(css);
25
- if (output.errors.length > 0) {
26
- console.error('CSS Minification Errors:', output.errors);
27
- return null;
28
- }
29
- return output.styles;
28
+ return context.chain.cssOutput;
30
29
  };
31
30
 
32
- // FUNCTION TO CONVERT JS CODES TO CSS CODE
33
- const processor = (inputFile, outputFile) => {
34
- const allowedExtensions = ['.jcss'];
35
- const fileExt = path.extname(inputFile).toLowerCase();
36
-
37
- if (!allowedExtensions.includes(fileExt)) {
38
- throw new Error(`Invalid file extension: ${fileExt}. Only .jcss files are allowed.`);
39
- }
40
- const input = path.resolve(inputFile);
41
- const content = fs.readFileSync(input, 'utf8');
31
+ // FUNCTION TO CONVERT JS CODES TO CSS CODE (FIRST STEP)
32
+ const processJavascriptBlocks = (content) => {
42
33
  const blocks = content.split(/<@([\s\S]*?)@>/gm);
43
34
  let outputCSS = '';
44
35
 
@@ -53,43 +44,235 @@ const processor = (inputFile, outputFile) => {
53
44
  outputCSS += outputProcessScript;
54
45
  }
55
46
  } catch (err) {
56
- console.error(`Error processing script block in ${inputFile}:`, err.stack);
57
- throw err; // Stop the process by re-throwing the error
47
+ console.error(`Error processing script block:`, err.stack);
48
+ throw err;
58
49
  }
59
50
  }
60
51
  }
61
- const outputDir = path.resolve(outputFile);
62
- const trimmedCSS = outputCSS.trim();
63
- const minCSS = minifyCss(trimmedCSS);
64
- fs.writeFileSync(outputDir, minCSS, 'utf8');
52
+
53
+ return outputCSS.trim();
54
+ };
55
+
56
+ // NEW: Validate CSS (check for unclosed blocks)
57
+ const validateCSS = (css) => {
58
+ const openBraces = (css.match(/{/g) || []).length;
59
+ const closeBraces = (css.match(/}/g) || []).length;
60
+
61
+ if (openBraces !== closeBraces) {
62
+ console.error(`CSS syntax error: Unclosed blocks (${openBraces} opening vs ${closeBraces} closing braces)`);
63
+ return false;
64
+ }
65
+ return true;
66
+ };
67
+
68
+ // Modified minification function with source map support
69
+
70
+ const processAndMinifyCss = async (css, inputFile, outputFile) => {
71
+ // First validate the CSS
72
+ if (!validateCSS(css)) {
73
+ throw new Error('Invalid CSS syntax - check for missing braces');
74
+ }
75
+
76
+ // Step 1: Apply prefixer (if enabled)
77
+ let processedCss = css;
78
+ let sourceMap = null;
79
+
80
+ if (prefixerConfig.enabled) {
81
+ try {
82
+ const result = await prefixer.process(css, {
83
+ from: inputFile,
84
+ to: outputFile,
85
+ map: prefixerConfig.sourceMap !== false
86
+ });
87
+
88
+ processedCss = result.css;
89
+ sourceMap = result.map;
90
+
91
+ } catch (err) {
92
+ console.error('⚠Prefixer error:', err.message);
93
+ processedCss = css;
94
+ }
95
+ }
96
+
97
+ // Step 2: Minify
98
+ const output = new CleanCSS().minify(processedCss);
99
+ if (output.errors.length > 0) {
100
+ console.error('CSS Minification Errors:', output.errors);
101
+ return { css: null, map: null };
102
+ }
103
+
104
+ let finalCss = output.styles;
105
+
106
+ if (sourceMap && !prefixerConfig.sourceMapInline) {
107
+ const mapFileName = path.basename(`${outputFile}.map`);
108
+ finalCss += `\n/*# sourceMappingURL=${mapFileName} */`;
109
+ }
110
+
111
+ return { css: finalCss, map: sourceMap };
112
+ };
113
+
114
+ // Main processor function - FIXED ORDER
115
+
116
+ const processor = async (inputFile, outputFile) => {
117
+ try {
118
+ const input = path.resolve(inputFile);
119
+ const output = path.resolve(outputFile);
120
+ const content = fs.readFileSync(input, 'utf8');
121
+
122
+ // STEP 1: Process JavaScript blocks first
123
+ const processedCSS = processJavascriptBlocks(content);
124
+
125
+ // STEP 2: Validate the CSS
126
+ if (!validateCSS(processedCSS)) {
127
+ throw new Error('Invalid CSS syntax');
128
+ }
129
+
130
+ // STEP 3: Apply prefixer and minify with source maps
131
+
132
+ const result = await processAndMinifyCss(processedCSS, input, output);
133
+ if (result.css) {
134
+ fs.writeFileSync(output, result.css, 'utf8');
135
+
136
+ //Write source map if generated
137
+ if (result.map) {
138
+ const mapFile = `${output}.map`;
139
+ fs.writeFileSync(mapFile, result.map, 'utf8');
140
+ }
141
+
142
+ if (prefixerConfig.enabled) {
143
+ console.log(` Prefixer: ${prefixerConfig.mode} mode (${prefixerConfig.browsers.join(', ')})`);
144
+ }
145
+ //Show source map status
146
+ if (result.map) {
147
+ console.log(` Source maps: enabled`);
148
+ }
149
+ }
150
+ } catch (err) {
151
+ console.error(`Failed to process ${inputFile}:`, err.message);
152
+ throw err;
153
+ }
65
154
  };
66
155
 
67
156
  // Watch function
68
157
  function watch(inputFile, outputFile) {
69
158
  console.log(`Watching for changes in ${inputFile}...`);
70
- chokidar.watch(inputFile).on('change', () => {
159
+ chokidar.watch(inputFile).on('change', async () => {
71
160
  console.log(`File changed: ${inputFile}`);
72
- processor(inputFile, outputFile);
161
+ try {
162
+ await processor(inputFile, outputFile);
163
+ } catch (err) {
164
+ console.error('Error during watch processing:', err);
165
+ }
73
166
  });
74
167
  }
75
168
 
169
+ // Parse CLI arguments
170
+
171
+ function parseArgs(args) {
172
+ const result = {
173
+ inputFile: null,
174
+ outputFile: null,
175
+ watchMode: false,
176
+ noPrefix: false,
177
+ browsers: null,
178
+ prefixerMode: 'auto',
179
+ // NEW: Add these
180
+ sourceMap: true, // Enable source maps by default
181
+ sourceMapInline: false
182
+ };
183
+
184
+ for (let i = 0; i < args.length; i++) {
185
+ const arg = args[i];
186
+
187
+ if (arg === '--watch') {
188
+ result.watchMode = true;
189
+ } else if (arg === '--no-prefix') {
190
+ result.noPrefix = true;
191
+ } else if (arg === '--prefixer-mode' && args[i + 1]) {
192
+ result.prefixerMode = args[i + 1];
193
+ i++;
194
+ } else if (arg === '--browsers' && args[i + 1]) {
195
+ result.browsers = args[i + 1].split(',');
196
+ i++;
197
+ // NEW: Add these two
198
+ } else if (arg === '--no-source-map') {
199
+ result.sourceMap = false;
200
+ } else if (arg === '--source-map-inline') {
201
+ result.sourceMapInline = true;
202
+ } else if (!result.inputFile) {
203
+ result.inputFile = arg;
204
+ } else if (!result.outputFile) {
205
+ result.outputFile = arg;
206
+ }
207
+ }
208
+
209
+ return result;
210
+ }
211
+
76
212
  // Main CLI logic
77
213
  if (require.main === module) {
78
214
  const args = process.argv.slice(2);
79
- const inputFile = args[0];
80
- const outputFile = args[1];
81
- const watchMode = args.includes('--watch');
215
+ const cliOptions = parseArgs(args);
216
+
217
+ if (!cliOptions.inputFile || !cliOptions.outputFile) {
218
+ console.log(`
219
+ ChainCSS - JavaScript-powered CSS preprocessor
82
220
 
83
- if (!inputFile || !outputFile) {
84
- console.error('Usage: chaincss <inputFile> <outputFile> [--watch]');
221
+ Usage:
222
+ chaincss <inputFile> <outputFile> [options]
223
+
224
+ Options:
225
+ --watch Watch for changes
226
+ --no-prefix Disable automatic prefixing
227
+ --browsers <list> Browser support list (comma-separated)
228
+ Example: --browsers ">1%,last 2 versions,not IE 11"
229
+
230
+ Examples:
231
+ chaincss style.jcss style.css
232
+ chaincss style.jcss style.css --watch
233
+ chaincss style.jcss style.css --browsers ">5%,last 2 safari versions"
234
+ chaincss style.jcss style.css --no-prefix
235
+ `);
85
236
  process.exit(1);
86
237
  }
87
238
 
88
- processor(inputFile, outputFile);
239
+ // sourceMap
240
+ if (cliOptions.sourceMap !== undefined) {
241
+ prefixerConfig.sourceMap = cliOptions.sourceMap;
242
+ }
243
+ if (cliOptions.sourceMapInline) {
244
+ prefixerConfig.sourceMapInline = true;
245
+ }
89
246
 
90
- if (watchMode) {
91
- watch(inputFile, outputFile);
247
+ // Then apply to prefixer:
248
+ if (cliOptions.prefixerMode) {
249
+ prefixerConfig.mode = cliOptions.prefixerMode;
92
250
  }
251
+
252
+ // Apply CLI options
253
+ if (cliOptions.noPrefix) {
254
+ prefixerConfig.enabled = false;
255
+ }
256
+
257
+ if (cliOptions.browsers) {
258
+ prefixerConfig.browsers = cliOptions.browsers;
259
+ // Re-initialize prefixer with new config
260
+ Object.assign(prefixer, new ChainCSSPrefixer(prefixerConfig));
261
+ }
262
+
263
+ // Run processor
264
+ (async () => {
265
+ try {
266
+ await processor(cliOptions.inputFile, cliOptions.outputFile);
267
+
268
+ if (cliOptions.watchMode) {
269
+ watch(cliOptions.inputFile, cliOptions.outputFile);
270
+ }
271
+ } catch (err) {
272
+ console.error('Fatal error:', err);
273
+ process.exit(1);
274
+ }
275
+ })();
93
276
  }
94
277
 
95
- module.exports = { processor, watch };
278
+ module.exports = { processor, watch };
package/package.json CHANGED
@@ -1,15 +1,44 @@
1
1
  {
2
2
  "name": "@melcanz85/chaincss",
3
- "version": "1.5.10",
3
+ "version": "1.6.0",
4
4
  "description": "A simple package transpiler for js to css",
5
5
  "main": "chaincss.js",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/melcanz08/chaincss.git"
13
+ },
14
+ "homepage": "https://github.com/melcanz08/chaincss",
6
15
  "bin": {
7
- "chaincss": "./chaincss.js"
16
+ "chaincss": "chaincss.js"
8
17
  },
9
18
  "dependencies": {
10
19
  "chokidar": "^3.5.3",
11
20
  "clean-css": "^5.3.3"
12
21
  },
22
+ "peerDependencies": {
23
+ "autoprefixer": "^10.0.0",
24
+ "browserslist": "^4.28.1",
25
+ "caniuse-db": "^1.0.30001770",
26
+ "postcss": "^8.5.6"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "autoprefixer": {
30
+ "optional": true
31
+ },
32
+ "postcss": {
33
+ "optional": true
34
+ },
35
+ "browserslist": {
36
+ "optional": true
37
+ },
38
+ "caniuse-db": {
39
+ "optional": true
40
+ }
41
+ },
13
42
  "keywords": [
14
43
  "css",
15
44
  "js",
package/prefixer.js ADDED
@@ -0,0 +1,297 @@
1
+ let postcss, browserslist, caniuse, autoprefixer;
2
+
3
+ // Try to load optional dependencies
4
+ try {
5
+ postcss = require('postcss');
6
+ browserslist = require('browserslist');
7
+ caniuse = require('caniuse-db/fulldata-json/data-2.0.json');
8
+ } catch (err) {
9
+ // Optional deps not installed - will use lightweight mode
10
+ }
11
+
12
+ // Try to load Autoprefixer (optional)
13
+ try {
14
+ autoprefixer = require('autoprefixer');
15
+ } catch (err) {
16
+ // Autoprefixer not installed - will use built-in
17
+ }
18
+
19
+ class ChainCSSPrefixer {
20
+ constructor(config = {}) {
21
+ this.config = {
22
+ browsers: config.browsers || ['> 0.5%', 'last 2 versions', 'not dead'],
23
+ enabled: config.enabled !== false,
24
+ mode: config.mode || 'auto',
25
+ sourceMap: config.sourceMap !== false, // Enable source maps by default
26
+ sourceMapInline: config.sourceMapInline || false,
27
+ ...config
28
+ };
29
+
30
+ // Check what's available
31
+ this.hasBuiltInDeps = !!(postcss && browserslist && caniuse);
32
+ this.hasAutoprefixer = !!autoprefixer;
33
+
34
+ // Determine which mode to use
35
+ this.prefixerMode = this.determineMode();
36
+
37
+ // Built-in prefixer data
38
+ this.caniuseData = caniuse ? caniuse.data : null;
39
+ this.commonProperties = this.getCommonProperties();
40
+ this.specialValues = {
41
+ 'display': ['flex', 'inline-flex', 'grid', 'inline-grid'],
42
+ 'background-clip': ['text'],
43
+ 'position': ['sticky']
44
+ };
45
+
46
+ this.browserPrefixMap = {
47
+ 'chrome': 'webkit', 'safari': 'webkit', 'firefox': 'moz',
48
+ 'ie': 'ms', 'edge': 'webkit', 'ios_saf': 'webkit',
49
+ 'and_chr': 'webkit', 'android': 'webkit', 'opera': 'webkit',
50
+ 'op_mob': 'webkit', 'samsung': 'webkit', 'and_ff': 'moz'
51
+ };
52
+
53
+ this.targetBrowsers = null;
54
+ }
55
+
56
+
57
+ determineMode() {
58
+ // User explicitly wants full mode but Autoprefixer not installed
59
+ if (this.config.mode === 'full' && !this.hasAutoprefixer) {
60
+ console.warn('⚠️ Full mode requested but autoprefixer not installed. Falling back to lightweight mode.');
61
+ console.warn(' To use full mode: npm install autoprefixer postcss');
62
+ return 'lightweight';
63
+ }
64
+
65
+ // User explicitly wants lightweight mode
66
+ if (this.config.mode === 'lightweight') {
67
+ return 'lightweight';
68
+ }
69
+
70
+ // User wants full mode and it's available
71
+ if (this.config.mode === 'full' && this.hasAutoprefixer) {
72
+ return 'full';
73
+ }
74
+
75
+ // Auto mode: use full if available, otherwise lightweight
76
+ if (this.config.mode === 'auto') {
77
+ return this.hasAutoprefixer ? 'full' : 'lightweight';
78
+ }
79
+
80
+ return 'lightweight';
81
+ }
82
+
83
+ async process(cssString, options = {}) {
84
+ if (!this.config.enabled) {
85
+ return { css: cssString, map: null };
86
+ }
87
+
88
+ try {
89
+ // Set up source map options
90
+ const mapOptions = {
91
+ inline: this.config.sourceMapInline,
92
+ annotation: false, // We'll add the comment ourselves
93
+ sourcesContent: true
94
+ };
95
+
96
+ if (this.prefixerMode === 'full') {
97
+ return await this.processWithAutoprefixer(cssString, options, mapOptions);
98
+ }
99
+
100
+ return await this.processWithBuiltIn(cssString, options, mapOptions);
101
+
102
+ } catch (err) {
103
+ console.error('⚠️ Prefixer error:', err.message);
104
+ return { css: cssString, map: null };
105
+ }
106
+ }
107
+
108
+ // 🚀 Full mode with Autoprefixer
109
+ async processWithAutoprefixer(cssString, options, mapOptions) {
110
+ const from = options.from || 'input.css';
111
+ const to = options.to || 'output.css';
112
+
113
+ const result = await postcss([
114
+ autoprefixer({ overrideBrowserslist: this.config.browsers })
115
+ ]).process(cssString, {
116
+ from,
117
+ to,
118
+ map: this.config.sourceMap ? mapOptions : false
119
+ });
120
+
121
+ return {
122
+ css: result.css,
123
+ map: result.map ? result.map.toString() : null
124
+ };
125
+ }
126
+
127
+ // 🔧 Lightweight mode with built-in prefixer
128
+ async processWithBuiltIn(cssString, options, mapOptions) {
129
+ if (!this.hasBuiltInDeps) {
130
+ return { css: cssString, map: null };
131
+ }
132
+
133
+ this.targetBrowsers = browserslist(this.config.browsers);
134
+
135
+ const from = options.from || 'input.css';
136
+ const to = options.to || 'output.css';
137
+
138
+ const result = await postcss([
139
+ this.createBuiltInPlugin()
140
+ ]).process(cssString, {
141
+ from,
142
+ to,
143
+ map: this.config.sourceMap ? mapOptions : false
144
+ });
145
+
146
+ return {
147
+ css: result.css,
148
+ map: result.map ? result.map.toString() : null
149
+ };
150
+ }
151
+
152
+ createBuiltInPlugin() {
153
+ return (root) => {
154
+ root.walkDecls(decl => {
155
+ this.processBuiltInDeclaration(decl);
156
+ });
157
+ };
158
+ }
159
+
160
+ processBuiltInDeclaration(decl) {
161
+ const { prop, value } = decl;
162
+
163
+ if (this.commonProperties.includes(prop)) {
164
+ this.addPrefixesFromCaniuse(decl);
165
+ }
166
+
167
+ if (this.specialValues[prop]?.includes(value)) {
168
+ this.addSpecialValuePrefixes(decl);
169
+ }
170
+ }
171
+
172
+ addPrefixesFromCaniuse(decl) {
173
+ if (!this.caniuseData) return;
174
+
175
+ const feature = this.findFeature(decl.prop);
176
+ if (!feature) return;
177
+
178
+ const prefixes = new Set();
179
+
180
+ this.targetBrowsers.forEach(browser => {
181
+ const [id, versionStr] = browser.split(' ');
182
+ const version = parseFloat(versionStr.split('-')[0]);
183
+ const stats = feature.stats[id];
184
+
185
+ if (stats) {
186
+ const versions = Object.keys(stats)
187
+ .map(v => parseFloat(v.split('-')[0]))
188
+ .filter(v => !isNaN(v))
189
+ .sort((a, b) => a - b);
190
+
191
+ const closestVersion = versions.find(v => v <= version) || versions[0];
192
+
193
+ if (closestVersion) {
194
+ const support = stats[closestVersion.toString()];
195
+ if (support && support.includes('x')) {
196
+ const prefix = this.browserPrefixMap[id.split('-')[0]];
197
+ if (prefix) prefixes.add(prefix);
198
+ }
199
+ }
200
+ }
201
+ });
202
+
203
+ prefixes.forEach(prefix => {
204
+ decl.cloneBefore({
205
+ prop: `-${prefix}-${decl.prop}`,
206
+ value: decl.value
207
+ });
208
+ });
209
+ }
210
+
211
+ addSpecialValuePrefixes(decl) {
212
+ const { prop, value } = decl;
213
+
214
+ if (prop === 'display') {
215
+ if (value === 'flex' || value === 'inline-flex') {
216
+ decl.cloneBefore({ prop: 'display', value: `-webkit-${value}` });
217
+ decl.cloneBefore({
218
+ prop: 'display',
219
+ value: value === 'flex' ? '-ms-flexbox' : '-ms-inline-flexbox'
220
+ });
221
+ }
222
+ if (value === 'grid' || value === 'inline-grid') {
223
+ decl.cloneBefore({
224
+ prop: 'display',
225
+ value: value === 'grid' ? '-ms-grid' : '-ms-inline-grid'
226
+ });
227
+ }
228
+ }
229
+
230
+ if (prop === 'background-clip' && value === 'text') {
231
+ decl.cloneBefore({ prop: '-webkit-background-clip', value: 'text' });
232
+ }
233
+
234
+ if (prop === 'position' && value === 'sticky') {
235
+ decl.cloneBefore({ prop: 'position', value: '-webkit-sticky' });
236
+ }
237
+ }
238
+
239
+ findFeature(property) {
240
+ if (!this.caniuseData) return null;
241
+
242
+ const featureMap = {
243
+ 'transform': 'transforms2d',
244
+ 'transform-origin': 'transforms2d',
245
+ 'transform-style': 'transforms3d',
246
+ 'perspective': 'transforms3d',
247
+ 'backface-visibility': 'transforms3d',
248
+ 'transition': 'css-transitions',
249
+ 'animation': 'css-animation',
250
+ 'backdrop-filter': 'backdrop-filter',
251
+ 'filter': 'css-filters',
252
+ 'user-select': 'user-select-none',
253
+ 'appearance': 'css-appearance',
254
+ 'mask-image': 'css-masks',
255
+ 'box-shadow': 'css-boxshadow',
256
+ 'border-radius': 'border-radius',
257
+ 'text-fill-color': 'text-stroke',
258
+ 'text-stroke': 'text-stroke',
259
+ 'background-clip': 'background-img-opts',
260
+ 'flex': 'flexbox',
261
+ 'flex-grow': 'flexbox',
262
+ 'flex-shrink': 'flexbox',
263
+ 'flex-basis': 'flexbox',
264
+ 'justify-content': 'flexbox',
265
+ 'align-items': 'flexbox',
266
+ 'grid': 'css-grid',
267
+ 'grid-template': 'css-grid',
268
+ 'grid-column': 'css-grid',
269
+ 'grid-row': 'css-grid'
270
+ };
271
+
272
+ const featureId = featureMap[property];
273
+ return featureId ? this.caniuseData[featureId] : null;
274
+ }
275
+
276
+ getCommonProperties() {
277
+ return [
278
+ 'transform', 'transform-origin', 'transform-style',
279
+ 'transition', 'transition-property', 'transition-duration', 'transition-timing-function',
280
+ 'animation', 'animation-name', 'animation-duration', 'animation-timing-function',
281
+ 'animation-delay', 'animation-iteration-count', 'animation-direction',
282
+ 'animation-fill-mode', 'animation-play-state',
283
+ 'backdrop-filter', 'filter',
284
+ 'user-select', 'appearance',
285
+ 'text-fill-color', 'text-stroke', 'text-stroke-color', 'text-stroke-width',
286
+ 'background-clip',
287
+ 'mask-image', 'mask-clip', 'mask-composite', 'mask-origin',
288
+ 'mask-position', 'mask-repeat', 'mask-size',
289
+ 'box-shadow', 'border-radius', 'box-sizing',
290
+ 'display', 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
291
+ 'justify-content', 'align-items', 'align-self', 'align-content',
292
+ 'grid', 'grid-template', 'grid-column', 'grid-row'
293
+ ];
294
+ }
295
+ }
296
+
297
+ module.exports = ChainCSSPrefixer;
package/transpiler.js CHANGED
@@ -12,6 +12,54 @@ const chain = {
12
12
  bgAttachment(bga){ this.catcher.backgroundAttachment = bga; return this; },
13
13
  bgPosition(bgp){ this.catcher.backgroundPosition = bgp; return this; },
14
14
 
15
+ // Border
16
+ border(b){ this.catcher.border = b; return this; },
17
+ borderStyle(bs){ this.catcher.borderStyle = bs; return this; },
18
+ borderWidth(bw){ this.catcher.borderWidth = bw; return this; },
19
+ borderColor(bc){ this.catcher.borderColor = bc; return this; },
20
+ borderRadius(br){ this.catcher.borderRadius = br; return this; },
21
+ borderSideStyle(side, value){
22
+ if (side === 'top') {
23
+ this.catcher.borderTopStyle = value;
24
+ } else if (side === 'right') {
25
+ this.catcher.borderRightStyle = value;
26
+ } else if (side === 'bottom') {
27
+ this.catcher.borderBottomStyle = value;
28
+ } else if (side === 'left') {
29
+ this.catcher.borderLeftStyle = value;
30
+ }
31
+ return this;
32
+ },
33
+
34
+ // Margin
35
+ margin(m){ this.catcher.margin = m; return this; },
36
+ marginTop(mt){ this.catcher.marginTop = mt; return this; },
37
+ marginRight(mr){ this.catcher.marginRight = mr; return this; },
38
+ marginBottom(mb){ this.catcher.marginBottom = mb; return this; },
39
+ marginLeft(ml){ this.catcher.marginLeft = ml; return this; },
40
+
41
+ // Padding
42
+ padding(p){ this.catcher.padding = p; return this; },
43
+ paddingTop(pt){ this.catcher.paddingTop = pt; return this; },
44
+ paddingRight(pr){ this.catcher.paddingRight = pr; return this; },
45
+ paddingBottom(pb){ this.catcher.paddingBottom = pb; return this; },
46
+ paddingLeft(pl){ this.catcher.paddingLeft = pl; return this; },
47
+
48
+ // Height, Width and Max-width
49
+ width(w){ this.catcher.width = w; return this; },
50
+ minWidth(mnw){ this.catcher.minWidth = mnw; return this; },
51
+ maxWidth(mxw){ this.catcher.maxWidth = mxw; return this; },
52
+ height(h){ this.catcher.height = h; return this; },
53
+ minHeight(mnh){ this.catcher.minHeight = mnh; return this; },
54
+ maxHeight(mxh){ this.catcher.maxHeight = mxh; return this; },
55
+
56
+ // Outline
57
+ outline(o){ this.catcher.outline = o; return this; },
58
+ outlineColor(oc){ this.catcher.outlineColor = oc; return this; },
59
+ outlineStyle(os){ this.catcher.outlineStyle = os; return this; },
60
+ outlineWidth(ow){ this.catcher.outlineWidth = ow; return this; },
61
+ outlineOffset(oo){ this.catcher.outlineOffset = oo; return this; },
62
+
15
63
  // Text
16
64
  color(c){ this.catcher.color = c; return this; },
17
65
  direction(d){ this.catcher.direction = d; return this; },
@@ -37,31 +85,19 @@ const chain = {
37
85
  wordSpacing(ws){ this.catcher.wordSpacing = ws; return this; },
38
86
  whiteSpace(sws){ this.catcher.whiteSpace = sws; return this; },
39
87
 
40
- // Border
41
- border(b){ this.catcher.border = b; return this; },
42
- borderStyle(bs){ this.catcher.borderStyle = bs; return this; },
43
- borderWidth(bw){ this.catcher.borderWidth = bw; return this; },
44
- borderColor(bc){ this.catcher.borderColor = bc; return this; },
45
- borderRadius(br){ this.catcher.borderRadius = br; return this; },
46
- borderSideStyle(side, value){
47
- if (side === 'top') {
48
- this.catcher.borderTopStyle = value;
49
- } else if (side === 'right') {
50
- this.catcher.borderRightStyle = value;
51
- } else if (side === 'bottom') {
52
- this.catcher.borderBottomStyle = value;
53
- } else if (side === 'left') {
54
- this.catcher.borderLeftStyle = value;
55
- }
56
- return this;
57
- },
58
-
59
88
  // Font
60
- fontFamily(f){ this.catcher.fontFamily = f; return this; },
61
- fontStyle(s){ this.catcher.fontStyle = s; return this; },
62
- fontWeight(w){ this.catcher.fontWeight = w; return this; },
63
- fontVariant(v){ this.catcher.fontVariant = v; return this; },
64
- fontSize(si){ this.catcher.fontSize = si; return this; },
89
+ font(f){ this.catcher.font = f; return this; },
90
+ fontFamily(ff){ this.catcher.fontFamily = ff; return this; },
91
+ fontStyle(fs){ this.catcher.fontStyle = fs; return this; },
92
+ fontWeight(fw){ this.catcher.fontWeight = fw; return this; },
93
+ fontVariant(fv){ this.catcher.fontVariant = fv; return this; },
94
+ fontSize(fsz){ this.catcher.fontSize = fsz; return this; },
95
+
96
+ // List Style
97
+ listStyle(ls){ this.catcher.listStyle = ls; return this; },
98
+ listStyleType(lst){ this.catcher.listStyleType = lst; return this; },
99
+ listStyleImage(lsi){ this.catcher.listStyleImage = lsi; return this; },
100
+ listStylePosition(lsp){ this.catcher.listStylePosition = lsp; return this; },
65
101
 
66
102
  // Display
67
103
  display(d){ this.catcher.display = d; return this; },
@@ -76,27 +112,11 @@ const chain = {
76
112
  order(o){ this.catcher.order = o; return this; },
77
113
  visibility(v){ this.catcher.visibility = v; return this; },
78
114
 
79
- // Height, Width and Max-width
80
- width(w){ this.catcher.width = w; return this; },
81
- minWidth(mnw){ this.catcher.minWidth = mnw; return this; },
82
- maxWidth(mxw){ this.catcher.maxWidth = mxw; return this; },
83
- height(h){ this.catcher.height = h; return this; },
84
- minHeight(mnh){ this.catcher.minHeight = mnh; return this; },
85
- maxHeight(mxh){ this.catcher.maxHeight = mxh; return this; },
86
-
87
- // Padding
88
- padding(p){ this.catcher.padding = p; return this; },
89
- paddingTop(pt){ this.catcher.paddingTop = pt; return this; },
90
- paddingRight(pr){ this.catcher.paddingRight = pr; return this; },
91
- paddingBottom(pb){ this.catcher.paddingBottom = pb; return this; },
92
- paddingLeft(pl){ this.catcher.paddingLeft = pl; return this; },
93
-
94
- // Margin
95
- margin(m){ this.catcher.margin = m; return this; },
96
- marginTop(mt){ this.catcher.marginTop = mt; return this; },
97
- marginRight(mr){ this.catcher.marginRight = mr; return this; },
98
- marginBottom(mb){ this.catcher.marginBottom = mb; return this; },
99
- marginLeft(ml){ this.catcher.marginLeft = ml; return this; },
115
+ // Position
116
+ position(p){ this.catcher.position = p; return this; },
117
+ top(t){ this.catcher.top = t; return this; },
118
+ left(l){ this.catcher.left = l; return this; },
119
+ bottom(b){ this.catcher.bottom = b; return this; },
100
120
 
101
121
  // Overflow
102
122
  overflow(o){ this.catcher.overflow = o; return this; },
@@ -104,28 +124,19 @@ const chain = {
104
124
  overflowY(oy){ this.catcher.overflowY = oy; return this; },
105
125
  overflowWrap(ow){ this.catcher.overflowWrap = ow; return this; },
106
126
 
107
- // List Style
108
- listStyle(ls){ this.catcher.listStyle = ls; return this; },
109
- listStyleType(lst){ this.catcher.listStyleType = lst; return this; },
110
- listStyleImage(lsi){ this.catcher.listStyleImage = lsi; return this; },
111
- listStylePosition(lsp){ this.catcher.listStylePosition = lsp; return this; },
112
-
113
- // Outline
114
- outline(o){ this.catcher.outline = o; return this; },
115
- outlineColor(oc){ this.catcher.outlineColor = oc; return this; },
116
- outlineStyle(os){ this.catcher.outlineStyle = os; return this; },
117
- outlineWidth(ow){ this.catcher.outlineWidth = ow; return this; },
118
- outlineOffset(oo){ this.catcher.outlineOffset = oo; return this; },
127
+ animation(a){ this.catcher.animation = a; return this; },
128
+ textFillColor(tfc){ this.catcher.textFillColor = tfc; return this; },
129
+ backgroundClip(bc){ this.catcher.backgroundClip = bc; return this; },
130
+ gridTemplateColumns(gtc){ this.catcher.gridTemplateColumns = gtc; return this; },
131
+ right(r){ this.catcher.right = r; return this; },
132
+ transform(tf){ this.catcher.transform = tf; return this; },
133
+ boxShadow(bs){ this.catcher.boxShadow = bs; return this; },
134
+ backdropFilter(bf){ this.catcher.backdropFilter = bf; return this; },
135
+ gap(g){ this.catcher.gap = g; return this; },
119
136
 
120
137
  // Float
121
138
  float(f){ this.catcher.float = f; return this; },
122
139
  clear(c){ this.catcher.clear = c; return this; },
123
-
124
- // Position
125
- position(p){ this.catcher.position = p; return this; },
126
- top(t){ this.catcher.top = t; return this; },
127
- left(l){ this.catcher.left = l; return this; },
128
- bottom(b){ this.catcher.bottom = b; return this; },
129
140
 
130
141
  // Z-index
131
142
  zIndex(zi){ this.catcher.zIndex = zi; return this; },
@@ -197,9 +208,6 @@ const compile = (obj) => {
197
208
  const element = obj[key];
198
209
  let selectors = element.selectors || []; // Provide default empty array if selectors is undefined
199
210
  let elementCSS = '';
200
- console.log('Problematic element:', element);
201
- console.log('Type of element:', typeof element);
202
- console.log('Is element null?', element === null);
203
211
  for (let prop in element) {
204
212
 
205
213
  if (element.hasOwnProperty(prop) && prop !== 'selectors') {
@@ -218,9 +226,6 @@ const compile = (obj) => {
218
226
  };
219
227
 
220
228
  const get = (filename) => {
221
- console.log('get() called with:', filename);
222
- console.log('Current working directory:', process.cwd());
223
-
224
229
  const fileExt = path.extname(filename).toLowerCase();
225
230
  if (fileExt !== '.jcss') {
226
231
  throw new Error(`Import error: ${filename} must have .jcss extension`);
@@ -228,11 +233,9 @@ const get = (filename) => {
228
233
 
229
234
  // Try to resolve the path
230
235
  const resolvedPath = path.resolve(process.cwd(), filename);
231
- console.log('Resolved path:', resolvedPath);
232
236
 
233
237
  // Check if file exists
234
238
  const exists = fs.existsSync(resolvedPath);
235
- console.log('File exists?', exists);
236
239
 
237
240
  if (!exists) {
238
241
  throw new Error(`File not found: ${filename} (resolved to: ${resolvedPath})`);
@@ -245,7 +248,6 @@ if (typeof global !== 'undefined') {
245
248
  global.chain = chain;
246
249
  }
247
250
 
248
-
249
251
  module.exports = {
250
252
  chain,
251
253
  run,