@melcanz85/chaincss 1.5.27 → 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.
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,94 +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
- //*** 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 standard. Example the css 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.
64
-
65
- //*** 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.
66
-
67
-
68
- //--Chaining File (chaincss/chain.jcss):
69
-
70
- // Variables for consistent styling
71
- const bodyBgColor = '#f0f0f0';
72
- const headerBgColor = '#333';
73
- const bodyFontFamily = 'Arial, sans-serif';
74
- const headerAlignItems = 'center';
75
- const logoHeight = '50px';
76
-
77
- // Reset browser defaults
78
- const resetDefaultBrowStyle = chain
79
- .margin('0')
80
- .padding('0')
81
- .block('body', 'h1', 'h2', 'h3', 'p', 'ul');
82
-
83
- // Body styles
84
- const bodyStyle = chain
85
- .fontFamily(bodyFontFamily)
86
- .lineHeight('1.6')
87
- .bgColor(bodyBgColor)
88
- .block('body');
89
-
90
- // Header styles
91
- const header = chain
92
- .display('flex')
93
- .alignItems(headerAlignItems)
94
- .justifyContent('space-between')
95
- .bgColor(headerBgColor)
96
- .color('#fff')
97
- .padding('10px 20px')
98
- .block('header');
99
-
100
- // Logo
101
- const logoImgHeight = chain
102
- .height(logoHeight)
103
- .block('.logo img');
104
-
105
- module.exports = {
106
- resetDefaultBrowStyle,
107
- bodyStyle,
108
- header,
109
- logoImgHeight
110
- };
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
+ };
111
149
 
112
150
 
113
151
  //--Main File (chaincss/main.jcss):
114
152
 
115
- <@
116
- // Import chaining definitions
117
- const style = get('./chain.jcss');
118
-
119
- // Override specific styles
120
- style.header.bgColor = 'red';
121
-
122
- // Compile to CSS
123
- compile(style);
124
- @>
125
-
126
- @media (max-width: 768px) {
127
- <@
128
- run(
129
- chain.flexDirection('column').alignItems('flex-start').block('header'),
130
- chain.order(1).block('.logo'),
131
- chain.order(2).block('.search-bar'),
132
- chain.order(3).block('h1'),
133
- chain.order(5).block('nav'),
134
- chain.order(4).display('flex').width('100%').justifyContent('flex-end').block('.burgerWrapper')
135
- );
136
- @>
137
- }
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
+ }
138
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
+ }
139
185
 
140
186
  📝 Notes
141
187
 
@@ -143,13 +189,16 @@ module.exports = {
143
189
 
144
190
  But chainCSS syntax must be wrapped in <@ @> delimiters.
145
191
 
146
- The get() function imports chaining definitions from other files
192
+ The get() function imports chaining definitions from your chain.jcss file
147
193
 
148
- 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.
149
196
 
150
197
  🎨 Editor Support
151
198
 
152
- 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
+
153
202
  VS Code
154
203
 
155
204
  Add this to your project's .vscode/settings.json:
@@ -171,13 +220,13 @@ WebStorm / IntelliJ IDEA
171
220
  Vim / Neovim
172
221
 
173
222
  Add to your .vimrc or init.vim:
174
- vim
175
223
 
176
- au BufRead,BufNewFile *.jcss setfiletype javascript
224
+ au BufRead,BufNewFile *.jcss setfiletype javascript
177
225
 
178
226
  Sublime Text
179
227
 
180
- 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
+
181
230
  json
182
231
 
183
232
  {
@@ -190,12 +239,13 @@ Atom
190
239
  Add to your config.cson:
191
240
  coffeescript
192
241
 
193
- "*":
194
- core:
195
- customFileTypes:
196
- "source.js": [
197
- "jcss"
198
- ]
242
+ "*":
243
+ core:
244
+ customFileTypes:
245
+ "source.js": [
246
+ "jcss"
247
+ ]
248
+
199
249
 
200
250
  Other Editors
201
251
 
@@ -208,19 +258,17 @@ Status Feature Description
208
258
 
209
259
  ✅ Basic JS → CSS Convert plain JS objects to CSS
210
260
 
211
- 🚧 Keyframe animations @keyframes support
212
-
213
- 🚧 Vendor prefixing Auto-add -webkit-, -moz-, etc.
214
-
215
- 🚧 Source maps Debug generated CSS
261
+ Vendor prefixing Auto-add -webkit-, -moz-, etc.
216
262
 
217
- 🚧 Watch mode Auto-recompile on file changes
218
-
219
- = Working, 🚧 = Coming soon
263
+ Keyframe animations @keyframes support
264
+
265
+ Source maps Debug generated CSS
220
266
 
267
+ ✅ Watch mode Auto-recompile on file changes
221
268
 
222
269
  👨‍💻 Contributing
223
- 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.
224
272
 
225
273
  📄 License
226
274
 
package/chaincss.js CHANGED
@@ -7,39 +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
20
  const processScript = (scriptBlock) => {
12
- //const output = 'cssOutput = undefined;';
13
21
  const context = vm.createContext({
14
- ...transpilerModule // Only expose what's needed
22
+ ...transpilerModule
15
23
  });
16
24
 
17
- const jsCode = scriptBlock.trim(); //`(function() { ${scriptBlock.trim()} })();`; Wrap script in IIFE
25
+ const jsCode = scriptBlock.trim();
18
26
  const chainScript = new vm.Script(jsCode);
19
- chainScript.runInContext(context); // Execute in isolated context
20
- return context.chain.cssOutput; // Return the processed output
21
- };
22
-
23
- // CSS Minification Function
24
- const minifyCss = (css) => {
25
- const output = new CleanCSS().minify(css);
26
- if (output.errors.length > 0) {
27
- console.error('CSS Minification Errors:', output.errors);
28
- return null;
29
- }
30
- return output.styles;
27
+ chainScript.runInContext(context);
28
+ return context.chain.cssOutput;
31
29
  };
32
30
 
33
- // FUNCTION TO CONVERT JS CODES TO CSS CODE
34
- const processor = (inputFile, outputFile) => {
35
- /*const allowedExtensions = ['.jcss'];
36
- const fileExt = path.extname(inputFile).toLowerCase();
37
-
38
- if (!allowedExtensions.includes(fileExt)) {
39
- throw new Error(`Invalid file extension: ${fileExt}. Only .jcss files are allowed.`);
40
- }*/
41
- const input = path.resolve(inputFile);
42
- const content = fs.readFileSync(input, 'utf8');
31
+ // FUNCTION TO CONVERT JS CODES TO CSS CODE (FIRST STEP)
32
+ const processJavascriptBlocks = (content) => {
43
33
  const blocks = content.split(/<@([\s\S]*?)@>/gm);
44
34
  let outputCSS = '';
45
35
 
@@ -54,43 +44,235 @@ const processor = (inputFile, outputFile) => {
54
44
  outputCSS += outputProcessScript;
55
45
  }
56
46
  } catch (err) {
57
- console.error(`Error processing script block in ${inputFile}:`, err.stack);
58
- throw err; // Stop the process by re-throwing the error
47
+ console.error(`Error processing script block:`, err.stack);
48
+ throw err;
59
49
  }
60
50
  }
61
51
  }
62
- const outputDir = path.resolve(outputFile);
63
- const trimmedCSS = outputCSS.trim();
64
- const minCSS = minifyCss(trimmedCSS);
65
- fs.writeFileSync(outputDir, minCSS, 'utf8'); // Write processed CSS
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
+ }
66
154
  };
67
155
 
68
156
  // Watch function
69
157
  function watch(inputFile, outputFile) {
70
158
  console.log(`Watching for changes in ${inputFile}...`);
71
- chokidar.watch(inputFile).on('change', () => {
159
+ chokidar.watch(inputFile).on('change', async () => {
72
160
  console.log(`File changed: ${inputFile}`);
73
- processor(inputFile, outputFile);
161
+ try {
162
+ await processor(inputFile, outputFile);
163
+ } catch (err) {
164
+ console.error('Error during watch processing:', err);
165
+ }
74
166
  });
75
167
  }
76
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
+
77
212
  // Main CLI logic
78
213
  if (require.main === module) {
79
214
  const args = process.argv.slice(2);
80
- const inputFile = args[0];
81
- const outputFile = args[1];
82
- 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
220
+
221
+ Usage:
222
+ chaincss <inputFile> <outputFile> [options]
83
223
 
84
- if (!inputFile || !outputFile) {
85
- console.error('Usage: chaincss <inputFile> <outputFile> [--watch]');
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
+ `);
86
236
  process.exit(1);
87
237
  }
88
238
 
89
- 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
+ }
246
+
247
+ // Then apply to prefixer:
248
+ if (cliOptions.prefixerMode) {
249
+ prefixerConfig.mode = cliOptions.prefixerMode;
250
+ }
90
251
 
91
- if (watchMode) {
92
- watch(inputFile, outputFile);
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));
93
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
+ })();
94
276
  }
95
277
 
96
- module.exports = { processor, watch };
278
+ module.exports = { processor, watch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melcanz85/chaincss",
3
- "version": "1.5.27",
3
+ "version": "1.6.0",
4
4
  "description": "A simple package transpiler for js to css",
5
5
  "main": "chaincss.js",
6
6
  "publishConfig": {
@@ -19,6 +19,26 @@
19
19
  "chokidar": "^3.5.3",
20
20
  "clean-css": "^5.3.3"
21
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
+ },
22
42
  "keywords": [
23
43
  "css",
24
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
@@ -124,6 +124,7 @@ const chain = {
124
124
  overflowY(oy){ this.catcher.overflowY = oy; return this; },
125
125
  overflowWrap(ow){ this.catcher.overflowWrap = ow; return this; },
126
126
 
127
+ animation(a){ this.catcher.animation = a; return this; },
127
128
  textFillColor(tfc){ this.catcher.textFillColor = tfc; return this; },
128
129
  backgroundClip(bc){ this.catcher.backgroundClip = bc; return this; },
129
130
  gridTemplateColumns(gtc){ this.catcher.gridTemplateColumns = gtc; return this; },