@melcanz85/chaincss 1.12.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/node/benchmarks/compare.js +17 -0
- package/node/benchmarks/run.js +299 -0
- package/node/loaders/chaincss-loader.js +62 -0
- package/node/plugins/next-plugin.js +29 -0
- package/node/plugins/vite-plugin.js +215 -0
- package/node/plugins/webpack-plugin.js +41 -0
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rommel Caneos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
EOF
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChainCSS Benchmark Comparison
|
|
5
|
+
* Run with: npm run benchmark:compare
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
console.log('📊 ChainCSS Benchmark Comparison\n');
|
|
9
|
+
console.log('This will compare ChainCSS with other CSS-in-JS libraries.\n');
|
|
10
|
+
console.log('To run full benchmarks: npm run benchmark');
|
|
11
|
+
console.log('Check results at: https://chaincss.dev/benchmarks\n');
|
|
12
|
+
|
|
13
|
+
// Simple version info
|
|
14
|
+
const { version } = require('../../package.json');
|
|
15
|
+
console.log(`ChainCSS v${version} is ready for benchmarking!\n`);
|
|
16
|
+
console.log('For detailed benchmarks, please visit our documentation site.');
|
|
17
|
+
EOF
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// benchmarks/run.js
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const libraries = [
|
|
8
|
+
{
|
|
9
|
+
name: 'ChainCSS (Build)',
|
|
10
|
+
setup: () => {
|
|
11
|
+
const content = `
|
|
12
|
+
const button = $()
|
|
13
|
+
.backgroundColor('blue')
|
|
14
|
+
.color('white')
|
|
15
|
+
.padding('8px 16px')
|
|
16
|
+
.borderRadius('4px')
|
|
17
|
+
.hover().backgroundColor('darkblue').end()
|
|
18
|
+
.block('.btn');
|
|
19
|
+
module.exports = { button };
|
|
20
|
+
`;
|
|
21
|
+
fs.writeFileSync('./test.jcss', content);
|
|
22
|
+
},
|
|
23
|
+
build: () => {
|
|
24
|
+
execSync('npx @melcanz85/chaincss ./test.jcss ./dist --atomic', { stdio: 'pipe' });
|
|
25
|
+
},
|
|
26
|
+
cleanup: () => {
|
|
27
|
+
fs.unlinkSync('./test.jcss');
|
|
28
|
+
if (fs.existsSync('./dist')) {
|
|
29
|
+
fs.rmSync('./dist', { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'ChainCSS (Runtime)',
|
|
35
|
+
setup: () => {
|
|
36
|
+
const content = `
|
|
37
|
+
import { $ } from '@melcanz85/chaincss';
|
|
38
|
+
const button = $()
|
|
39
|
+
.backgroundColor('blue')
|
|
40
|
+
.color('white')
|
|
41
|
+
.padding('8px 16px')
|
|
42
|
+
.borderRadius('4px')
|
|
43
|
+
.hover().backgroundColor('darkblue').end()
|
|
44
|
+
.block('.btn');
|
|
45
|
+
export default button;
|
|
46
|
+
`;
|
|
47
|
+
fs.writeFileSync('./test.js', content);
|
|
48
|
+
},
|
|
49
|
+
build: () => {
|
|
50
|
+
// No build step for runtime
|
|
51
|
+
},
|
|
52
|
+
cleanup: () => {
|
|
53
|
+
fs.unlinkSync('./test.js');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'CSS Modules',
|
|
58
|
+
setup: () => {
|
|
59
|
+
const content = `
|
|
60
|
+
.btn {
|
|
61
|
+
background-color: blue;
|
|
62
|
+
color: white;
|
|
63
|
+
padding: 8px 16px;
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
}
|
|
66
|
+
.btn:hover {
|
|
67
|
+
background-color: darkblue;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
fs.writeFileSync('./test.module.css', content);
|
|
71
|
+
},
|
|
72
|
+
build: () => {
|
|
73
|
+
// CSS Modules are handled by webpack
|
|
74
|
+
},
|
|
75
|
+
cleanup: () => {
|
|
76
|
+
fs.unlinkSync('./test.module.css');
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Vanilla Extract',
|
|
81
|
+
setup: () => {
|
|
82
|
+
const content = `
|
|
83
|
+
import { style } from '@vanilla-extract/css';
|
|
84
|
+
export const button = style({
|
|
85
|
+
backgroundColor: 'blue',
|
|
86
|
+
color: 'white',
|
|
87
|
+
padding: '8px 16px',
|
|
88
|
+
borderRadius: '4px',
|
|
89
|
+
':hover': {
|
|
90
|
+
backgroundColor: 'darkblue'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
`;
|
|
94
|
+
fs.writeFileSync('./test.css.ts', content);
|
|
95
|
+
},
|
|
96
|
+
build: () => {
|
|
97
|
+
execSync('npx esbuild ./test.css.ts --bundle --outfile=./dist/test.js', { stdio: 'pipe' });
|
|
98
|
+
},
|
|
99
|
+
cleanup: () => {
|
|
100
|
+
fs.unlinkSync('./test.css.ts');
|
|
101
|
+
if (fs.existsSync('./dist')) {
|
|
102
|
+
fs.rmSync('./dist', { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'Styled Components',
|
|
108
|
+
setup: () => {
|
|
109
|
+
const content = `
|
|
110
|
+
import styled from 'styled-components';
|
|
111
|
+
export const Button = styled.button\`
|
|
112
|
+
background-color: blue;
|
|
113
|
+
color: white;
|
|
114
|
+
padding: 8px 16px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
&:hover {
|
|
117
|
+
background-color: darkblue;
|
|
118
|
+
}
|
|
119
|
+
\`;
|
|
120
|
+
`;
|
|
121
|
+
fs.writeFileSync('./test.jsx', content);
|
|
122
|
+
},
|
|
123
|
+
build: () => {
|
|
124
|
+
execSync('npx esbuild ./test.jsx --bundle --outfile=./dist/test.js', { stdio: 'pipe' });
|
|
125
|
+
},
|
|
126
|
+
cleanup: () => {
|
|
127
|
+
fs.unlinkSync('./test.jsx');
|
|
128
|
+
if (fs.existsSync('./dist')) {
|
|
129
|
+
fs.rmSync('./dist', { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
async function runBenchmarks() {
|
|
136
|
+
console.log('\n🚀 Running ChainCSS Performance Benchmarks\n');
|
|
137
|
+
console.log('═'.repeat(60));
|
|
138
|
+
|
|
139
|
+
const results = [];
|
|
140
|
+
|
|
141
|
+
for (const lib of libraries) {
|
|
142
|
+
console.log(`\n📊 Testing ${lib.name}...`);
|
|
143
|
+
|
|
144
|
+
// Setup
|
|
145
|
+
lib.setup();
|
|
146
|
+
|
|
147
|
+
// Measure build time
|
|
148
|
+
const start = performance.now();
|
|
149
|
+
try {
|
|
150
|
+
await lib.build();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(` ❌ Failed: ${err.message}`);
|
|
153
|
+
lib.cleanup();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const buildTime = performance.now() - start;
|
|
157
|
+
|
|
158
|
+
// Measure bundle size
|
|
159
|
+
let bundleSize = 0;
|
|
160
|
+
let cssSize = 0;
|
|
161
|
+
|
|
162
|
+
if (lib.name === 'ChainCSS (Build)') {
|
|
163
|
+
if (fs.existsSync('./dist/global.css')) {
|
|
164
|
+
cssSize = fs.statSync('./dist/global.css').size;
|
|
165
|
+
}
|
|
166
|
+
} else if (lib.name === 'ChainCSS (Runtime)') {
|
|
167
|
+
const bundle = fs.readFileSync('./test.js', 'utf8');
|
|
168
|
+
bundleSize = bundle.length;
|
|
169
|
+
} else if (lib.name === 'CSS Modules') {
|
|
170
|
+
// CSS Modules size is just the CSS file
|
|
171
|
+
cssSize = fs.statSync('./test.module.css').size;
|
|
172
|
+
} else if (fs.existsSync('./dist/test.js')) {
|
|
173
|
+
bundleSize = fs.statSync('./dist/test.js').size;
|
|
174
|
+
if (fs.existsSync('./dist/test.css')) {
|
|
175
|
+
cssSize = fs.statSync('./dist/test.css').size;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
results.push({
|
|
180
|
+
name: lib.name,
|
|
181
|
+
buildTime: buildTime.toFixed(2),
|
|
182
|
+
bundleSize: formatBytes(bundleSize),
|
|
183
|
+
cssSize: formatBytes(cssSize),
|
|
184
|
+
totalSize: formatBytes(bundleSize + cssSize)
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Cleanup
|
|
188
|
+
lib.cleanup();
|
|
189
|
+
|
|
190
|
+
console.log(` ✅ Build: ${buildTime.toFixed(2)}ms`);
|
|
191
|
+
console.log(` 📦 Bundle: ${formatBytes(bundleSize)}`);
|
|
192
|
+
console.log(` 🎨 CSS: ${formatBytes(cssSize)}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Generate report
|
|
196
|
+
generateReport(results);
|
|
197
|
+
generateChart(results);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatBytes(bytes) {
|
|
201
|
+
if (bytes === 0) return '0 B';
|
|
202
|
+
const k = 1024;
|
|
203
|
+
const sizes = ['B', 'KB', 'MB'];
|
|
204
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
205
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function generateReport(results) {
|
|
209
|
+
const markdown = `# ChainCSS Performance Benchmarks
|
|
210
|
+
|
|
211
|
+
## Test Environment
|
|
212
|
+
- **OS**: ${os.type()} ${os.release()}
|
|
213
|
+
- **CPU**: ${os.cpus()[0].model}
|
|
214
|
+
- **RAM**: ${formatBytes(os.totalmem())}
|
|
215
|
+
- **Node**: ${process.version}
|
|
216
|
+
- **Date**: ${new Date().toISOString()}
|
|
217
|
+
|
|
218
|
+
## Results
|
|
219
|
+
|
|
220
|
+
| Library | Build Time | Bundle Size | CSS Size | Total Size |
|
|
221
|
+
|---------|------------|-------------|----------|------------|
|
|
222
|
+
${results.map(r => `| ${r.name} | ${r.buildTime}ms | ${r.bundleSize} | ${r.cssSize} | ${r.totalSize} |`).join('\n')}
|
|
223
|
+
|
|
224
|
+
## Key Findings
|
|
225
|
+
|
|
226
|
+
${generateInsights(results)}
|
|
227
|
+
|
|
228
|
+
## Recommendations
|
|
229
|
+
|
|
230
|
+
${generateRecommendations(results)}
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
fs.writeFileSync('./benchmarks/results.md', markdown);
|
|
234
|
+
console.log('\n✅ Report saved to benchmarks/results.md');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function generateInsights(results) {
|
|
238
|
+
const chaincssBuild = results.find(r => r.name === 'ChainCSS (Build)');
|
|
239
|
+
const chaincssRuntime = results.find(r => r.name === 'ChainCSS (Runtime)');
|
|
240
|
+
const cssModules = results.find(r => r.name === 'CSS Modules');
|
|
241
|
+
const vanillaExtract = results.find(r => r.name === 'Vanilla Extract');
|
|
242
|
+
const styledComponents = results.find(r => r.name === 'Styled Components');
|
|
243
|
+
|
|
244
|
+
let insights = [];
|
|
245
|
+
|
|
246
|
+
if (chaincssBuild && cssModules) {
|
|
247
|
+
insights.push(`- **ChainCSS (Build)** is ${((parseFloat(cssModules.buildTime) - parseFloat(chaincssBuild.buildTime)) / parseFloat(cssModules.buildTime) * 100).toFixed(0)}% faster than CSS Modules`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (chaincssBuild && vanillaExtract) {
|
|
251
|
+
insights.push(`- **ChainCSS (Build)** is ${((parseFloat(vanillaExtract.buildTime) - parseFloat(chaincssBuild.buildTime)) / parseFloat(vanillaExtract.buildTime) * 100).toFixed(0)}% faster than Vanilla Extract`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (chaincssRuntime && styledComponents) {
|
|
255
|
+
insights.push(`- **ChainCSS (Runtime)** is ${((parseFloat(styledComponents.bundleSize) - parseFloat(chaincssRuntime.bundleSize)) / parseFloat(styledComponents.bundleSize) * 100).toFixed(0)}% smaller than Styled Components`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return insights.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function generateRecommendations(results) {
|
|
262
|
+
return `
|
|
263
|
+
### For Static Sites
|
|
264
|
+
**Use ChainCSS (Build Mode)** - ${results.find(r => r.name === 'ChainCSS (Build)')?.buildTime}ms build time, zero runtime
|
|
265
|
+
|
|
266
|
+
### For Dynamic Apps
|
|
267
|
+
**Use ChainCSS (Runtime Mode)** - ${results.find(r => r.name === 'ChainCSS (Runtime)')?.bundleSize} bundle size, full dynamic capability
|
|
268
|
+
|
|
269
|
+
### For Best Performance
|
|
270
|
+
**Use ChainCSS with Atomic CSS** - Automatic optimization with ${results.find(r => r.name === 'ChainCSS (Build)')?.cssSize} CSS output
|
|
271
|
+
|
|
272
|
+
### For Component Libraries
|
|
273
|
+
**Use ChainCSS Recipe System** - Built-in variants, compound styles, zero config
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function generateChart(results) {
|
|
278
|
+
const chartData = {
|
|
279
|
+
labels: results.map(r => r.name),
|
|
280
|
+
datasets: [
|
|
281
|
+
{
|
|
282
|
+
label: 'Build Time (ms)',
|
|
283
|
+
data: results.map(r => parseFloat(r.buildTime)),
|
|
284
|
+
backgroundColor: 'rgba(102, 126, 234, 0.5)'
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
label: 'Bundle Size (KB)',
|
|
288
|
+
data: results.map(r => parseFloat(r.bundleSize)),
|
|
289
|
+
backgroundColor: 'rgba(118, 75, 162, 0.5)'
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync('./benchmarks/chart-data.json', JSON.stringify(chartData, null, 2));
|
|
295
|
+
console.log('📊 Chart data saved to benchmarks/chart-data.json');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Run benchmarks
|
|
299
|
+
runBenchmarks().catch(console.error);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// node/loaders/chaincss-loader.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
module.exports = function(source) {
|
|
7
|
+
const callback = this.async();
|
|
8
|
+
const options = this.getOptions() || {};
|
|
9
|
+
|
|
10
|
+
const mode = options.mode || (process.env.NODE_ENV === 'production' ? 'build' : 'runtime');
|
|
11
|
+
|
|
12
|
+
if (mode === 'runtime') {
|
|
13
|
+
const code = `
|
|
14
|
+
import { $, compile } from '@melcanz85/chaincss';
|
|
15
|
+
const styles = (() => {
|
|
16
|
+
${source}
|
|
17
|
+
return { ${extractStyleNames(source)} };
|
|
18
|
+
})();
|
|
19
|
+
export default styles;
|
|
20
|
+
`;
|
|
21
|
+
callback(null, code);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const tempFile = path.join(this.context, '.temp.jcss');
|
|
27
|
+
fs.writeFileSync(tempFile, source);
|
|
28
|
+
|
|
29
|
+
const outputDir = path.join(process.cwd(), '.chaincss-cache');
|
|
30
|
+
if (!fs.existsSync(outputDir)) {
|
|
31
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cmd = `node ${path.join(__dirname, '../chaincss.js')} ${tempFile} ${outputDir} ${options.atomic ? '--atomic' : ''}`;
|
|
35
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
36
|
+
|
|
37
|
+
const cssPath = path.join(outputDir, 'global.css');
|
|
38
|
+
const css = fs.readFileSync(cssPath, 'utf8');
|
|
39
|
+
|
|
40
|
+
fs.unlinkSync(tempFile);
|
|
41
|
+
|
|
42
|
+
const code = `
|
|
43
|
+
const css = ${JSON.stringify(css)};
|
|
44
|
+
if (typeof document !== 'undefined') {
|
|
45
|
+
const style = document.createElement('style');
|
|
46
|
+
style.setAttribute('data-chaincss', ${JSON.stringify(this.resourcePath)});
|
|
47
|
+
style.textContent = css;
|
|
48
|
+
document.head.appendChild(style);
|
|
49
|
+
}
|
|
50
|
+
export default {};
|
|
51
|
+
`;
|
|
52
|
+
callback(null, code);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
callback(err);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function extractStyleNames(source) {
|
|
59
|
+
const matches = source.match(/const\s+(\w+)\s*=\s*\$\(\)/g);
|
|
60
|
+
if (!matches) return '';
|
|
61
|
+
return matches.map(m => m.match(/const\s+(\w+)/)[1]).join(', ');
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// node/plugins/next-plugin.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = function withChainCSS(nextConfig = {}) {
|
|
5
|
+
return {
|
|
6
|
+
...nextConfig,
|
|
7
|
+
webpack(config, options) {
|
|
8
|
+
config.module.rules.push({
|
|
9
|
+
test: /\.jcss$/,
|
|
10
|
+
use: [
|
|
11
|
+
options.defaultLoaders.babel,
|
|
12
|
+
{
|
|
13
|
+
loader: path.resolve(__dirname, '../loaders/chaincss-loader.js'),
|
|
14
|
+
options: {
|
|
15
|
+
mode: process.env.NODE_ENV === 'production' ? 'build' : 'runtime',
|
|
16
|
+
atomic: process.env.NODE_ENV === 'production'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (typeof nextConfig.webpack === 'function') {
|
|
23
|
+
return nextConfig.webpack(config, options);
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
},
|
|
27
|
+
pageExtensions: [...(nextConfig.pageExtensions || []), 'jcss']
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// node/plugins/vite-plugin.js
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import CleanCSS from 'clean-css';
|
|
6
|
+
import { $, run, compile as originalCompile, chain } from '../btt.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
// Optional: Try to load prefixer
|
|
11
|
+
let prefixer;
|
|
12
|
+
try {
|
|
13
|
+
const ChainCSSPrefixer = (await import('../prefixer.js')).default;
|
|
14
|
+
prefixer = new ChainCSSPrefixer({ enabled: true });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.warn('ChainCSS: Prefixer not available, autoprefixing disabled');
|
|
17
|
+
prefixer = { process: async (css) => ({ css }) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Cache for processed files
|
|
21
|
+
const fileCache = new Map();
|
|
22
|
+
const compiledCache = new Map();
|
|
23
|
+
|
|
24
|
+
// Helper to compile script without temp files
|
|
25
|
+
const compileScript = (scriptBlock, filename, get) => {
|
|
26
|
+
const dirname = path.dirname(filename);
|
|
27
|
+
|
|
28
|
+
// Reset CSS output
|
|
29
|
+
chain.cssOutput = '';
|
|
30
|
+
|
|
31
|
+
// Create a function from the script - no temp files!
|
|
32
|
+
const fn = new Function(
|
|
33
|
+
'$',
|
|
34
|
+
'run',
|
|
35
|
+
'compile',
|
|
36
|
+
'chain',
|
|
37
|
+
'get',
|
|
38
|
+
'__filename',
|
|
39
|
+
'__dirname',
|
|
40
|
+
scriptBlock
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Execute with helpers
|
|
44
|
+
fn($, run, originalCompile, chain, get, filename, dirname);
|
|
45
|
+
|
|
46
|
+
return chain.cssOutput || '';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const processJavascriptBlocks = (content, filename, get) => {
|
|
50
|
+
const blocks = content.split(/<@([\s\S]*?)@>/gm);
|
|
51
|
+
let output = '';
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
54
|
+
if (i % 2 === 0) {
|
|
55
|
+
// Static content
|
|
56
|
+
output += blocks[i];
|
|
57
|
+
} else {
|
|
58
|
+
// JavaScript block
|
|
59
|
+
const css = compileScript(blocks[i], filename, get);
|
|
60
|
+
if (css && typeof css === 'string') {
|
|
61
|
+
output += css;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return output.trim();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const processJCSSFile = (filePath) => {
|
|
70
|
+
const abs = path.resolve(filePath);
|
|
71
|
+
|
|
72
|
+
// Return cached result if available
|
|
73
|
+
if (fileCache.has(abs)) return fileCache.get(abs);
|
|
74
|
+
|
|
75
|
+
// Check if file exists
|
|
76
|
+
if (!fs.existsSync(abs)) {
|
|
77
|
+
throw new Error(`ChainCSS: File not found: ${abs}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
81
|
+
const dirname = path.dirname(abs);
|
|
82
|
+
|
|
83
|
+
// Create get function for this file
|
|
84
|
+
const get = (relativePath) => {
|
|
85
|
+
const targetPath = path.resolve(dirname, relativePath);
|
|
86
|
+
return processJCSSFile(targetPath);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Process the file
|
|
90
|
+
const result = processJavascriptBlocks(content, abs, get);
|
|
91
|
+
|
|
92
|
+
// Cache the result
|
|
93
|
+
fileCache.set(abs, result);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Minify and prefix CSS
|
|
98
|
+
const processCSS = async (css, filepath, options = {}) => {
|
|
99
|
+
const { minify = true, prefix = true } = options;
|
|
100
|
+
let processed = css;
|
|
101
|
+
|
|
102
|
+
// Add prefixing
|
|
103
|
+
if (prefix && prefixer) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await prefixer.process(css, { from: filepath });
|
|
106
|
+
processed = result.css;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.warn(`ChainCSS prefixer error in ${filepath}:`, err.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Minify
|
|
113
|
+
if (minify) {
|
|
114
|
+
const minified = new CleanCSS({ level: 2 }).minify(processed);
|
|
115
|
+
if (minified.errors.length) {
|
|
116
|
+
console.warn(`ChainCSS minification errors in ${filepath}:`, minified.errors);
|
|
117
|
+
}
|
|
118
|
+
return minified.styles;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return processed;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default function chaincssVite(opts = {}) {
|
|
125
|
+
const {
|
|
126
|
+
extension = '.jcss',
|
|
127
|
+
minify = process.env.NODE_ENV === 'production',
|
|
128
|
+
prefix = true,
|
|
129
|
+
hmr = true
|
|
130
|
+
} = opts;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
name: 'vite-plugin-chaincss',
|
|
134
|
+
enforce: 'pre',
|
|
135
|
+
|
|
136
|
+
// Transform .jcss files
|
|
137
|
+
async transform(code, id) {
|
|
138
|
+
if (!id.endsWith(extension)) return null;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Create get function for root file
|
|
142
|
+
const dirname = path.dirname(id);
|
|
143
|
+
const get = (relativePath) => {
|
|
144
|
+
const targetPath = path.resolve(dirname, relativePath);
|
|
145
|
+
return processJCSSFile(targetPath);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Process the file
|
|
149
|
+
let css = processJavascriptBlocks(code, id, get);
|
|
150
|
+
|
|
151
|
+
// Process CSS (prefix + minify)
|
|
152
|
+
css = await processCSS(css, id, { minify, prefix });
|
|
153
|
+
|
|
154
|
+
// In development, inject CSS for HMR
|
|
155
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
156
|
+
return {
|
|
157
|
+
code: `
|
|
158
|
+
// ChainCSS HMR
|
|
159
|
+
const id = ${JSON.stringify(id)};
|
|
160
|
+
const css = ${JSON.stringify(css)};
|
|
161
|
+
|
|
162
|
+
// Add style to head
|
|
163
|
+
let style = document.querySelector(\`style[data-chaincss="\${id}"]\`);
|
|
164
|
+
if (!style) {
|
|
165
|
+
style = document.createElement('style');
|
|
166
|
+
style.setAttribute('data-chaincss', id);
|
|
167
|
+
document.head.appendChild(style);
|
|
168
|
+
}
|
|
169
|
+
style.textContent = css;
|
|
170
|
+
|
|
171
|
+
// HMR handling
|
|
172
|
+
if (import.meta.hot) {
|
|
173
|
+
import.meta.hot.accept((newModule) => {
|
|
174
|
+
if (newModule?.default) {
|
|
175
|
+
style.textContent = newModule.default;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
import.meta.hot.dispose(() => {
|
|
180
|
+
style.remove();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default css;
|
|
185
|
+
`,
|
|
186
|
+
map: null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Production: just export CSS
|
|
191
|
+
return {
|
|
192
|
+
code: `export default ${JSON.stringify(css)};`,
|
|
193
|
+
map: null
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
} catch (err) {
|
|
197
|
+
this.error(`ChainCSS error in ${id}: ${err.message}`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Handle HMR updates
|
|
203
|
+
handleHotUpdate({ file, server }) {
|
|
204
|
+
if (file.endsWith(extension)) {
|
|
205
|
+
// Invalidate cache for changed file
|
|
206
|
+
fileCache.delete(file);
|
|
207
|
+
// Trigger reload
|
|
208
|
+
server.ws.send({
|
|
209
|
+
type: 'full-reload',
|
|
210
|
+
path: '*'
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// node/plugins/webpack-plugin.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
class ChainCSSWebpackPlugin {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
atomic: process.env.NODE_ENV === 'production',
|
|
10
|
+
input: './src/styles/main.jcss',
|
|
11
|
+
output: './dist',
|
|
12
|
+
...options
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply(compiler) {
|
|
17
|
+
compiler.hooks.beforeCompile.tapAsync('ChainCSSPlugin', async (params, callback) => {
|
|
18
|
+
try {
|
|
19
|
+
const inputPath = path.resolve(process.cwd(), this.options.input);
|
|
20
|
+
const outputPath = path.resolve(process.cwd(), this.options.output);
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(inputPath)) {
|
|
23
|
+
console.warn('ChainCSS: No main.jcss file found, skipping...');
|
|
24
|
+
callback();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cmd = `node ${path.join(__dirname, '../chaincss.js')} ${inputPath} ${outputPath} ${this.options.atomic ? '--atomic' : ''}`;
|
|
29
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
30
|
+
|
|
31
|
+
console.log('ChainCSS compiled successfully');
|
|
32
|
+
callback();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('ChainCSS compilation failed:', err.message);
|
|
35
|
+
callback(err);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = ChainCSSWebpackPlugin;
|