chaincss 2.0.6 ā 2.1.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/CHANGELOG.md +30 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +28 -0
- package/README.md +454 -231
- package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +1 -0
- package/demo/index.html +16 -0
- package/demo/package.json +20 -0
- package/demo/src/App.tsx +117 -0
- package/demo/src/chaincss-barrel.ts +9 -0
- package/demo/src/main.tsx +8 -0
- package/demo/src/styles.chain.ts +300 -0
- package/demo/vite.config.ts +46 -0
- package/dist/cli/commands/build.d.ts +0 -1
- package/dist/cli/commands/cache.d.ts +1 -0
- package/dist/cli/commands/init.d.ts +6 -3
- package/dist/cli/commands/timeline.d.ts +0 -1
- package/dist/cli/commands/watch.d.ts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +3213 -5296
- package/dist/cli/types.d.ts +51 -20
- package/dist/cli/utils/config-loader.d.ts +0 -1
- package/dist/cli/utils/file-utils.d.ts +27 -3
- package/dist/cli/utils/logger.d.ts +0 -1
- package/dist/compiler/Chain.d.ts +215 -0
- package/dist/compiler/animations.d.ts +76 -0
- package/dist/compiler/atomic-optimizer.d.ts +47 -12
- package/dist/compiler/breakpoints.d.ts +46 -0
- package/dist/compiler/btt.d.ts +36 -60
- package/dist/compiler/cache-manager.d.ts +58 -4
- package/dist/compiler/commonProps.d.ts +0 -1
- package/dist/compiler/content-addressable-cache.d.ts +78 -0
- package/dist/compiler/helpers.d.ts +54 -0
- package/dist/compiler/index.d.ts +16 -9
- package/dist/compiler/index.js +4450 -4316
- package/dist/compiler/prefixer.d.ts +17 -1
- package/dist/compiler/shorthands.d.ts +28 -0
- package/dist/compiler/suggestions.d.ts +43 -0
- package/dist/compiler/theme-contract.d.ts +16 -27
- package/dist/compiler/token-resolver.d.ts +69 -0
- package/dist/compiler/tokens.d.ts +33 -8
- package/dist/core/auto-detector.d.ts +34 -0
- package/dist/core/common-utils.d.ts +97 -0
- package/dist/core/compiler.d.ts +63 -23
- package/dist/core/constants.d.ts +137 -36
- package/dist/core/smart-chain.d.ts +3 -0
- package/dist/core/types.d.ts +122 -15
- package/dist/core/utils.d.ts +134 -17
- package/dist/index.d.ts +52 -8
- package/dist/index.js +7090 -5578
- package/dist/plugins/vite.d.ts +7 -5
- package/dist/plugins/vite.js +2964 -25641
- package/dist/plugins/webpack.d.ts +24 -1
- package/dist/plugins/webpack.js +209 -72
- package/dist/runtime/Chain.d.ts +32 -0
- package/dist/runtime/auto-hooks.d.ts +11 -0
- package/dist/runtime/hmr.d.ts +22 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +3649 -301
- package/dist/runtime/injector.d.ts +39 -71
- package/dist/runtime/react.d.ts +17 -12
- package/dist/runtime/svelte.d.ts +15 -0
- package/dist/runtime/types.d.ts +126 -4
- package/dist/runtime/utils.d.ts +0 -1
- package/dist/runtime/vue.d.ts +34 -14
- package/package.json +59 -66
- package/src/cli/commands/build.ts +133 -0
- package/src/cli/commands/cache.ts +371 -0
- package/src/cli/commands/init.ts +230 -0
- package/src/cli/commands/timeline.ts +435 -0
- package/src/cli/commands/watch.ts +211 -0
- package/src/cli/index.ts +226 -0
- package/src/cli/types.ts +100 -0
- package/src/cli/utils/config-loader.ts +174 -0
- package/src/cli/utils/file-utils.ts +139 -0
- package/src/cli/utils/logger.ts +74 -0
- package/src/compiler/Chain.ts +831 -0
- package/src/compiler/animations.ts +517 -0
- package/src/compiler/atomic-optimizer.ts +786 -0
- package/src/compiler/breakpoints.ts +347 -0
- package/src/compiler/btt.ts +1147 -0
- package/src/compiler/cache-manager.ts +446 -0
- package/src/compiler/commonProps.ts +18 -0
- package/src/compiler/content-addressable-cache.ts +478 -0
- package/src/compiler/helpers.ts +407 -0
- package/src/compiler/index.ts +72 -0
- package/src/compiler/prefixer.ts +724 -0
- package/src/compiler/shorthands.ts +558 -0
- package/src/compiler/suggestions.ts +436 -0
- package/src/compiler/theme-contract.ts +197 -0
- package/src/compiler/token-resolver.ts +241 -0
- package/src/compiler/tokens.ts +612 -0
- package/src/core/auto-detector.ts +187 -0
- package/src/core/common-utils.ts +423 -0
- package/src/core/compiler.ts +835 -0
- package/src/core/constants.ts +424 -0
- package/src/core/index.ts +107 -0
- package/src/core/smart-chain.ts +163 -0
- package/src/core/types.ts +257 -0
- package/src/core/utils.ts +598 -0
- package/src/index.ts +208 -0
- package/src/plugins/vite.d.ts +316 -0
- package/src/plugins/vite.ts +424 -0
- package/src/plugins/webpack.d.ts +289 -0
- package/src/plugins/webpack.ts +416 -0
- package/src/runtime/Chain.ts +242 -0
- package/src/runtime/auto-hooks.tsx +127 -0
- package/src/runtime/auto-vue.ts +72 -0
- package/src/runtime/hmr.ts +212 -0
- package/src/runtime/index.ts +82 -0
- package/src/runtime/injector.ts +273 -0
- package/src/runtime/react.tsx +269 -0
- package/src/runtime/svelte.ts +15 -0
- package/src/runtime/types.ts +256 -0
- package/src/runtime/utils.ts +128 -0
- package/src/runtime/vite-env.d.ts +120 -0
- package/src/runtime/vue.ts +231 -0
- package/tsconfig.build.json +41 -0
- package/tsconfig.json +25 -0
- package/tsconfig.runtimes.json +18 -0
- package/dist/cli/cli.cjs +0 -7
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/compile.d.ts +0 -3
- package/dist/cli/commands/compile.d.ts.map +0 -1
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/timeline.d.ts.map +0 -1
- package/dist/cli/commands/watch.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/utils/config-loader.d.ts.map +0 -1
- package/dist/cli/utils/file-utils.d.ts.map +0 -1
- package/dist/cli/utils/logger.d.ts.map +0 -1
- package/dist/compiler/atomic-optimizer.d.ts.map +0 -1
- package/dist/compiler/btt.d.ts.map +0 -1
- package/dist/compiler/cache-manager.d.ts.map +0 -1
- package/dist/compiler/commonProps.d.ts.map +0 -1
- package/dist/compiler/index.d.ts.map +0 -1
- package/dist/compiler/prefixer.d.ts.map +0 -1
- package/dist/compiler/theme-contract.d.ts.map +0 -1
- package/dist/compiler/tokens.d.ts.map +0 -1
- package/dist/compiler/types.d.ts +0 -57
- package/dist/compiler/types.d.ts.map +0 -1
- package/dist/core/compiler.d.ts.map +0 -1
- package/dist/core/constants.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/utils.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/plugins/vite.d.ts.map +0 -1
- package/dist/plugins/webpack.d.ts.map +0 -1
- package/dist/runtime/hmr.d.ts.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/injector.d.ts.map +0 -1
- package/dist/runtime/react.d.ts.map +0 -1
- package/dist/runtime/react.js +0 -270
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/utils.d.ts.map +0 -1
- package/dist/runtime/vue.d.ts.map +0 -1
- package/dist/runtime/vue.js +0 -232
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/cli/commands/init.ts
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { saveConfigTemplate } from '../utils/config-loader.js';
|
|
8
|
+
|
|
9
|
+
export interface InitOptions {
|
|
10
|
+
force?: boolean;
|
|
11
|
+
verbose?: boolean;
|
|
12
|
+
template?: 'full' | 'minimal';
|
|
13
|
+
typescript?: boolean;
|
|
14
|
+
framework?: 'react' | 'vue' | 'svelte' | 'solid';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function initCommand(options: InitOptions): Promise<void> {
|
|
18
|
+
const logger = createLogger(options.verbose);
|
|
19
|
+
|
|
20
|
+
logger.header('ChainCSS Initialization');
|
|
21
|
+
|
|
22
|
+
const configPath = path.join(process.cwd(), 'chaincss.config.js');
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
25
|
+
logger.warn('Config file already exists. Use --force to overwrite.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.step('Creating chaincss.config.js...');
|
|
30
|
+
|
|
31
|
+
// Save config template with options
|
|
32
|
+
saveConfigTemplate(configPath);
|
|
33
|
+
|
|
34
|
+
logger.success(`Created ${configPath}`);
|
|
35
|
+
|
|
36
|
+
// Create example style file
|
|
37
|
+
const exampleDir = path.join(process.cwd(), 'src');
|
|
38
|
+
const examplePath = path.join(exampleDir, 'styles.chain.js');
|
|
39
|
+
const isTypeScript = options.typescript || false;
|
|
40
|
+
const exampleExt = isTypeScript ? 'ts' : 'js';
|
|
41
|
+
const finalExamplePath = isTypeScript
|
|
42
|
+
? path.join(exampleDir, 'styles.chain.ts')
|
|
43
|
+
: examplePath;
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(finalExamplePath) || options.force) {
|
|
46
|
+
if (!fs.existsSync(exampleDir)) {
|
|
47
|
+
fs.mkdirSync(exampleDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Framework-specific example
|
|
51
|
+
let frameworkImport = "import { $, recipe } from 'chaincss';";
|
|
52
|
+
let frameworkSpecificCode = '';
|
|
53
|
+
|
|
54
|
+
if (options.framework === 'react') {
|
|
55
|
+
frameworkSpecificCode = `
|
|
56
|
+
// React component example
|
|
57
|
+
import React from 'react';
|
|
58
|
+
|
|
59
|
+
export const Button = ({ children, variant = 'primary' }) => {
|
|
60
|
+
const className = buttonVariants({ color: variant });
|
|
61
|
+
return <button className={className}>{children}</button>;
|
|
62
|
+
};`;
|
|
63
|
+
} else if (options.framework === 'vue') {
|
|
64
|
+
frameworkSpecificCode = `
|
|
65
|
+
// Vue component example
|
|
66
|
+
export default {
|
|
67
|
+
name: 'Button',
|
|
68
|
+
props: {
|
|
69
|
+
variant: { type: String, default: 'primary' }
|
|
70
|
+
},
|
|
71
|
+
computed: {
|
|
72
|
+
buttonClass() {
|
|
73
|
+
return buttonVariants({ color: this.variant });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};`;
|
|
77
|
+
} else if (options.framework === 'svelte') {
|
|
78
|
+
frameworkSpecificCode = `
|
|
79
|
+
<!-- Svelte component example -->
|
|
80
|
+
<script>
|
|
81
|
+
import { buttonVariants } from './styles.chain';
|
|
82
|
+
export let variant = 'primary';
|
|
83
|
+
$: buttonClass = buttonVariants({ color: variant });
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<button class={buttonClass}>
|
|
87
|
+
<slot />
|
|
88
|
+
</button>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const exampleContent = `/**
|
|
92
|
+
* ChainCSS Example Style File
|
|
93
|
+
*
|
|
94
|
+
* This file demonstrates ChainCSS syntax.
|
|
95
|
+
* Run \`npx chaincss build\` to compile all styles.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
${frameworkImport}
|
|
99
|
+
|
|
100
|
+
// Simple button style
|
|
101
|
+
export const button = $()
|
|
102
|
+
.backgroundColor('#667eea')
|
|
103
|
+
.color('white')
|
|
104
|
+
.padding('12px 24px')
|
|
105
|
+
.borderRadius('8px')
|
|
106
|
+
.border('none')
|
|
107
|
+
.fontSize('16px')
|
|
108
|
+
.fontWeight('600')
|
|
109
|
+
.cursor('pointer')
|
|
110
|
+
.transition('all 0.2s ease')
|
|
111
|
+
.hover()
|
|
112
|
+
.backgroundColor('#5a67d8')
|
|
113
|
+
.transform('scale(1.05)')
|
|
114
|
+
.end()
|
|
115
|
+
.$el('button');
|
|
116
|
+
|
|
117
|
+
// Card style
|
|
118
|
+
export const card = $()
|
|
119
|
+
.backgroundColor('white')
|
|
120
|
+
.borderRadius('12px')
|
|
121
|
+
.padding('24px')
|
|
122
|
+
.boxShadow('0 10px 15px -3px rgba(0,0,0,0.1)')
|
|
123
|
+
.transition('all 0.3s ease')
|
|
124
|
+
.hover()
|
|
125
|
+
.boxShadow('0 20px 25px -5px rgba(0,0,0,0.15)')
|
|
126
|
+
.transform('translateY(-4px)')
|
|
127
|
+
.end()
|
|
128
|
+
.$el('.card');
|
|
129
|
+
|
|
130
|
+
// Container style
|
|
131
|
+
export const container = $()
|
|
132
|
+
.maxWidth('1200px')
|
|
133
|
+
.margin('0 auto')
|
|
134
|
+
.padding('0 1rem')
|
|
135
|
+
.$el('.container');
|
|
136
|
+
|
|
137
|
+
// Responsive design
|
|
138
|
+
export const responsive = $()
|
|
139
|
+
.display('grid')
|
|
140
|
+
.gridTemplateColumns('1fr')
|
|
141
|
+
.gap('1rem')
|
|
142
|
+
.md()
|
|
143
|
+
.gridTemplateColumns('repeat(2, 1fr)')
|
|
144
|
+
.end()
|
|
145
|
+
.lg()
|
|
146
|
+
.gridTemplateColumns('repeat(3, 1fr)')
|
|
147
|
+
.end()
|
|
148
|
+
.$el('.grid');
|
|
149
|
+
|
|
150
|
+
// Variant-based button component
|
|
151
|
+
export const buttonVariants = recipe({
|
|
152
|
+
base: $()
|
|
153
|
+
.padding('8px 16px')
|
|
154
|
+
.borderRadius('4px')
|
|
155
|
+
.fontWeight('500')
|
|
156
|
+
.cursor('pointer')
|
|
157
|
+
.transition('all 0.2s')
|
|
158
|
+
.border('none')
|
|
159
|
+
.$el('button'),
|
|
160
|
+
|
|
161
|
+
variants: {
|
|
162
|
+
color: {
|
|
163
|
+
primary: $().backgroundColor('#667eea').color('white').hover().backgroundColor('#5a67d8').end().$el(),
|
|
164
|
+
secondary: $().backgroundColor('#48bb78').color('white').hover().backgroundColor('#38a169').end().$el(),
|
|
165
|
+
danger: $().backgroundColor('#f56565').color('white').hover().backgroundColor('#e53e3e').end().$el(),
|
|
166
|
+
outline: $()
|
|
167
|
+
.backgroundColor('transparent')
|
|
168
|
+
.color('#667eea')
|
|
169
|
+
.border('2px solid #667eea')
|
|
170
|
+
.hover()
|
|
171
|
+
.backgroundColor('#667eea')
|
|
172
|
+
.color('white')
|
|
173
|
+
.end()
|
|
174
|
+
.$el()
|
|
175
|
+
},
|
|
176
|
+
size: {
|
|
177
|
+
sm: $().padding('4px 8px').fontSize('12px').$el(),
|
|
178
|
+
md: $().padding('8px 16px').fontSize('14px').$el(),
|
|
179
|
+
lg: $().padding('12px 24px').fontSize('16px').$el()
|
|
180
|
+
},
|
|
181
|
+
fullWidth: {
|
|
182
|
+
true: $().width('100%').$el()
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
compoundVariants: [
|
|
187
|
+
{
|
|
188
|
+
variants: { color: 'outline', size: 'lg' },
|
|
189
|
+
style: $().borderWidth('3px').$el()
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
|
|
193
|
+
defaultVariants: {
|
|
194
|
+
color: 'primary',
|
|
195
|
+
size: 'md'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
${frameworkSpecificCode}
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
fs.writeFileSync(finalExamplePath, exampleContent, 'utf8');
|
|
202
|
+
logger.success(`Created example: ${finalExamplePath}`);
|
|
203
|
+
} else {
|
|
204
|
+
logger.info(`Example file already exists at ${finalExamplePath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Create .gitignore entry for cache if needed
|
|
208
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
209
|
+
const cacheIgnore = '\n# ChainCSS cache\n.chaincss-cache/\n.chaincss/\n';
|
|
210
|
+
|
|
211
|
+
if (fs.existsSync(gitignorePath)) {
|
|
212
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
213
|
+
if (!gitignore.includes('.chaincss-cache')) {
|
|
214
|
+
fs.appendFileSync(gitignorePath, cacheIgnore);
|
|
215
|
+
logger.info('Added cache directories to .gitignore');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
logger.divider();
|
|
220
|
+
logger.success('ChainCSS initialized successfully!');
|
|
221
|
+
logger.info('\nš Next steps:');
|
|
222
|
+
logger.info(` 1. Edit ${chalk.cyan('chaincss.config.js')} to customize settings`);
|
|
223
|
+
logger.info(` 2. Write styles in ${chalk.cyan(finalExamplePath)}`);
|
|
224
|
+
logger.info(` 3. Run ${chalk.cyan('npx chaincss build')} to compile all styles`);
|
|
225
|
+
logger.info('');
|
|
226
|
+
logger.info(` Or watch for changes:`);
|
|
227
|
+
logger.info(` ${chalk.cyan('npx chaincss watch')}`);
|
|
228
|
+
logger.info('');
|
|
229
|
+
logger.info(` For more information, visit: ${chalk.blue('https://chaincss.dev')}\n`);
|
|
230
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// chaincss/src/commands/timeline.ts
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
interface StyleSnapshot {
|
|
11
|
+
id: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
selector: string;
|
|
14
|
+
styles: Record<string, any>;
|
|
15
|
+
source: string;
|
|
16
|
+
hash: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface StyleChange {
|
|
20
|
+
id: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
selector: string;
|
|
23
|
+
property: string;
|
|
24
|
+
oldValue: any;
|
|
25
|
+
newValue: any;
|
|
26
|
+
type: 'add' | 'remove' | 'modify';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TimelineData {
|
|
30
|
+
history: StyleSnapshot[];
|
|
31
|
+
changes: StyleChange[];
|
|
32
|
+
exportedAt?: number;
|
|
33
|
+
stats?: {
|
|
34
|
+
totalSnapshots: number;
|
|
35
|
+
totalChanges: number;
|
|
36
|
+
firstRecorded: number;
|
|
37
|
+
lastRecorded: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper to format bytes
|
|
42
|
+
function formatBytes(bytes: number): string {
|
|
43
|
+
if (bytes === 0) return '0 Bytes';
|
|
44
|
+
const k = 1024;
|
|
45
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
46
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
47
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Helper to format duration
|
|
51
|
+
function formatDuration(ms: number): string {
|
|
52
|
+
if (ms < 1000) return `${ms}ms`;
|
|
53
|
+
const seconds = ms / 1000;
|
|
54
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
55
|
+
const minutes = seconds / 60;
|
|
56
|
+
if (minutes < 60) return `${minutes.toFixed(1)}min`;
|
|
57
|
+
const hours = minutes / 60;
|
|
58
|
+
return `${hours.toFixed(1)}h`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Load timeline data with validation
|
|
62
|
+
function loadTimelineData(timelineFile: string): TimelineData | null {
|
|
63
|
+
if (!fs.existsSync(timelineFile)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(timelineFile, 'utf8');
|
|
69
|
+
const data = JSON.parse(content);
|
|
70
|
+
|
|
71
|
+
// Validate data structure
|
|
72
|
+
if (!data.history || !Array.isArray(data.history)) {
|
|
73
|
+
throw new Error('Invalid timeline data: missing history array');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return data;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(chalk.red(`Failed to load timeline data: ${(error as Error).message}`));
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Display snapshot details
|
|
84
|
+
function displaySnapshotDetails(snapshot: StyleSnapshot, index: number): void {
|
|
85
|
+
const date = new Date(snapshot.timestamp).toLocaleString();
|
|
86
|
+
const propCount = Object.keys(snapshot.styles).length;
|
|
87
|
+
|
|
88
|
+
console.log(`\n${chalk.green(`[${index}]`)} ${chalk.white.bold(snapshot.selector)}`);
|
|
89
|
+
console.log(` ${chalk.gray(`ID: ${snapshot.id}`)}`);
|
|
90
|
+
console.log(` ${chalk.gray(`Time: ${date}`)}`);
|
|
91
|
+
console.log(` ${chalk.gray(`Source: ${snapshot.source}`)}`);
|
|
92
|
+
console.log(` ${chalk.gray(`Properties: ${propCount}`)}`);
|
|
93
|
+
console.log(` ${chalk.gray(`Hash: ${snapshot.hash.slice(0, 8)}...`)}`);
|
|
94
|
+
|
|
95
|
+
// Show first few properties as preview
|
|
96
|
+
const previewProps = Object.entries(snapshot.styles).slice(0, 5);
|
|
97
|
+
if (previewProps.length > 0) {
|
|
98
|
+
console.log(` ${chalk.dim('Preview:')}`);
|
|
99
|
+
for (const [prop, value] of previewProps) {
|
|
100
|
+
console.log(` ${chalk.blue(prop)}: ${chalk.yellow(value)}`);
|
|
101
|
+
}
|
|
102
|
+
if (Object.keys(snapshot.styles).length > 5) {
|
|
103
|
+
console.log(` ${chalk.dim(`... and ${Object.keys(snapshot.styles).length - 5} more`)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Calculate diff between two snapshots
|
|
109
|
+
function calculateDiff(snapshot1: StyleSnapshot, snapshot2: StyleSnapshot): {
|
|
110
|
+
added: Record<string, any>;
|
|
111
|
+
removed: Record<string, any>;
|
|
112
|
+
modified: Record<string, { old: any; new: any }>;
|
|
113
|
+
} {
|
|
114
|
+
const added: Record<string, any> = {};
|
|
115
|
+
const removed: Record<string, any> = {};
|
|
116
|
+
const modified: Record<string, { old: any; new: any }> = {};
|
|
117
|
+
|
|
118
|
+
// Find added and modified properties
|
|
119
|
+
for (const [key, value] of Object.entries(snapshot2.styles)) {
|
|
120
|
+
if (!(key in snapshot1.styles)) {
|
|
121
|
+
added[key] = value;
|
|
122
|
+
} else if (JSON.stringify(snapshot1.styles[key]) !== JSON.stringify(value)) {
|
|
123
|
+
modified[key] = {
|
|
124
|
+
old: snapshot1.styles[key],
|
|
125
|
+
new: value
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Find removed properties
|
|
131
|
+
for (const [key, value] of Object.entries(snapshot1.styles)) {
|
|
132
|
+
if (!(key in snapshot2.styles)) {
|
|
133
|
+
removed[key] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { added, removed, modified };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Display diff with colors
|
|
141
|
+
function displayDiff(diff: ReturnType<typeof calculateDiff>, selector1: string, selector2: string): void {
|
|
142
|
+
const { added, removed, modified } = diff;
|
|
143
|
+
|
|
144
|
+
const totalChanges = Object.keys(added).length + Object.keys(removed).length + Object.keys(modified).length;
|
|
145
|
+
|
|
146
|
+
if (totalChanges === 0) {
|
|
147
|
+
console.log(chalk.gray(' No changes detected'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Display removed properties
|
|
152
|
+
for (const [prop, value] of Object.entries(removed)) {
|
|
153
|
+
console.log(`${chalk.red('ā')} ${chalk.red(prop)}: ${chalk.red(String(value))}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Display added properties
|
|
157
|
+
for (const [prop, value] of Object.entries(added)) {
|
|
158
|
+
console.log(`${chalk.green('+')} ${chalk.green(prop)}: ${chalk.green(String(value))}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Display modified properties
|
|
162
|
+
for (const [prop, { old, new: newVal }] of Object.entries(modified)) {
|
|
163
|
+
console.log(`${chalk.yellow('~')} ${chalk.yellow(prop)}: ${chalk.red(String(old))} ā ${chalk.green(String(newVal))}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function timelineCommand(action: string, options: any) {
|
|
168
|
+
const timelineFile = path.join(process.cwd(), '.chaincss-timeline.json');
|
|
169
|
+
const data = loadTimelineData(timelineFile);
|
|
170
|
+
|
|
171
|
+
if (!data) {
|
|
172
|
+
console.log(chalk.yellow('\nā ļø No timeline data found.'));
|
|
173
|
+
console.log(chalk.gray(' Run your build with --timeline flag first:'));
|
|
174
|
+
console.log(chalk.cyan(' $ chaincss build --timeline\n'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Calculate stats if not present
|
|
179
|
+
if (!data.stats && data.history.length > 0) {
|
|
180
|
+
data.stats = {
|
|
181
|
+
totalSnapshots: data.history.length,
|
|
182
|
+
totalChanges: data.changes?.length || 0,
|
|
183
|
+
firstRecorded: data.history[0]?.timestamp || 0,
|
|
184
|
+
lastRecorded: data.history[data.history.length - 1]?.timestamp || 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
switch (action) {
|
|
189
|
+
case 'list':
|
|
190
|
+
console.log(chalk.cyan.bold('\nš Style Timeline History\n'));
|
|
191
|
+
console.log(chalk.gray(` Total Snapshots: ${data.stats?.totalSnapshots || data.history.length}`));
|
|
192
|
+
console.log(chalk.gray(` Total Changes: ${data.stats?.totalChanges || data.changes?.length || 0}`));
|
|
193
|
+
|
|
194
|
+
if (data.stats?.firstRecorded && data.stats?.lastRecorded) {
|
|
195
|
+
const duration = data.stats.lastRecorded - data.stats.firstRecorded;
|
|
196
|
+
console.log(chalk.gray(` Duration: ${formatDuration(duration)}`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.gray('\n Snapshots:\n'));
|
|
200
|
+
|
|
201
|
+
data.history.forEach((snapshot: StyleSnapshot, index: number) => {
|
|
202
|
+
displaySnapshotDetails(snapshot, index);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Show file size
|
|
206
|
+
try {
|
|
207
|
+
const stats = fs.statSync(timelineFile);
|
|
208
|
+
console.log(chalk.gray(`\nš Timeline file size: ${formatBytes(stats.size)}`));
|
|
209
|
+
} catch (e) {}
|
|
210
|
+
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'diff':
|
|
214
|
+
const id1 = options.snapshot1;
|
|
215
|
+
const id2 = options.snapshot2;
|
|
216
|
+
|
|
217
|
+
if (!id1 || !id2) {
|
|
218
|
+
console.log(chalk.red('\nā Both snapshot1 and snapshot2 are required for diff'));
|
|
219
|
+
console.log(chalk.gray(' Usage: chaincss timeline diff --snapshot1 <id> --snapshot2 <id>\n'));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Find snapshots by ID or selector
|
|
224
|
+
const snapshot1 = data.history.find((s: StyleSnapshot) =>
|
|
225
|
+
s.id === id1 || s.selector === id1
|
|
226
|
+
);
|
|
227
|
+
const snapshot2 = data.history.find((s: StyleSnapshot) =>
|
|
228
|
+
s.id === id2 || s.selector === id2
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (!snapshot1) {
|
|
232
|
+
console.log(chalk.red(`\nā Snapshot not found: ${id1}`));
|
|
233
|
+
console.log(chalk.gray(' Use "chaincss timeline list" to see available snapshots\n'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!snapshot2) {
|
|
238
|
+
console.log(chalk.red(`\nā Snapshot not found: ${id2}`));
|
|
239
|
+
console.log(chalk.gray(' Use "chaincss timeline list" to see available snapshots\n'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const diff = calculateDiff(snapshot1, snapshot2);
|
|
244
|
+
const totalChanges = Object.keys(diff.added).length +
|
|
245
|
+
Object.keys(diff.removed).length +
|
|
246
|
+
Object.keys(diff.modified).length;
|
|
247
|
+
|
|
248
|
+
console.log(chalk.cyan.bold(`\nš Diff: ${snapshot1.selector} ā ${snapshot2.selector}\n`));
|
|
249
|
+
console.log(chalk.gray(` Changes: ${totalChanges}`));
|
|
250
|
+
console.log(chalk.gray(` Time between: ${formatDuration(snapshot2.timestamp - snapshot1.timestamp)}\n`));
|
|
251
|
+
|
|
252
|
+
displayDiff(diff, snapshot1.selector, snapshot2.selector);
|
|
253
|
+
|
|
254
|
+
if (totalChanges > 0) {
|
|
255
|
+
console.log(chalk.gray(`\n Legend: ${chalk.red('ā')} removed ${chalk.green('+')} added ${chalk.yellow('~')} modified\n`));
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'changes':
|
|
260
|
+
console.log(chalk.cyan.bold('\nš Style Change History\n'));
|
|
261
|
+
|
|
262
|
+
if (!data.changes || data.changes.length === 0) {
|
|
263
|
+
console.log(chalk.gray(' No changes recorded\n'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Group changes by selector
|
|
268
|
+
const changesBySelector = new Map<string, StyleChange[]>();
|
|
269
|
+
for (const change of data.changes) {
|
|
270
|
+
if (!changesBySelector.has(change.selector)) {
|
|
271
|
+
changesBySelector.set(change.selector, []);
|
|
272
|
+
}
|
|
273
|
+
changesBySelector.get(change.selector)!.push(change);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const [selector, changes] of changesBySelector) {
|
|
277
|
+
console.log(chalk.white.bold(`\n ${selector}`));
|
|
278
|
+
|
|
279
|
+
for (const change of changes) {
|
|
280
|
+
const date = new Date(change.timestamp).toLocaleTimeString();
|
|
281
|
+
switch (change.type) {
|
|
282
|
+
case 'add':
|
|
283
|
+
console.log(` ${chalk.green(`+ ${change.property}: ${change.newValue}`)} ${chalk.gray(`[${date}]`)}`);
|
|
284
|
+
break;
|
|
285
|
+
case 'remove':
|
|
286
|
+
console.log(` ${chalk.red(`- ${change.property}: ${change.oldValue}`)} ${chalk.gray(`[${date}]`)}`);
|
|
287
|
+
break;
|
|
288
|
+
case 'modify':
|
|
289
|
+
console.log(` ${chalk.yellow(`~ ${change.property}: ${change.oldValue} ā ${change.newValue}`)} ${chalk.gray(`[${date}]`)}`);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
console.log();
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'stats':
|
|
298
|
+
console.log(chalk.cyan.bold('\nš Timeline Statistics\n'));
|
|
299
|
+
console.log(chalk.gray(` Total Snapshots: ${data.history.length}`));
|
|
300
|
+
console.log(chalk.gray(` Total Changes: ${data.changes?.length || 0}`));
|
|
301
|
+
|
|
302
|
+
if (data.history.length > 0) {
|
|
303
|
+
const firstSnapshot = data.history[0];
|
|
304
|
+
const lastSnapshot = data.history[data.history.length - 1];
|
|
305
|
+
const duration = lastSnapshot.timestamp - firstSnapshot.timestamp;
|
|
306
|
+
|
|
307
|
+
console.log(chalk.gray(` First Recorded: ${new Date(firstSnapshot.timestamp).toLocaleString()}`));
|
|
308
|
+
console.log(chalk.gray(` Last Recorded: ${new Date(lastSnapshot.timestamp).toLocaleString()}`));
|
|
309
|
+
console.log(chalk.gray(` Duration: ${formatDuration(duration)}`));
|
|
310
|
+
|
|
311
|
+
// Count changes by type
|
|
312
|
+
const changesByType = { add: 0, remove: 0, modify: 0 };
|
|
313
|
+
for (const change of data.changes || []) {
|
|
314
|
+
changesByType[change.type]++;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(chalk.gray(`\n Changes by Type:`));
|
|
318
|
+
console.log(chalk.green(` Added: ${changesByType.add}`));
|
|
319
|
+
console.log(chalk.red(` Removed: ${changesByType.remove}`));
|
|
320
|
+
console.log(chalk.yellow(` Modified: ${changesByType.modify}`));
|
|
321
|
+
|
|
322
|
+
// Most active selectors
|
|
323
|
+
const changesBySelector: Record<string, number> = {};
|
|
324
|
+
for (const change of data.changes || []) {
|
|
325
|
+
changesBySelector[change.selector] = (changesBySelector[change.selector] || 0) + 1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const topSelectors = Object.entries(changesBySelector)
|
|
329
|
+
.sort((a, b) => b[1] - a[1])
|
|
330
|
+
.slice(0, 5);
|
|
331
|
+
|
|
332
|
+
if (topSelectors.length > 0) {
|
|
333
|
+
console.log(chalk.gray(`\n Most Active Selectors:`));
|
|
334
|
+
for (const [selector, count] of topSelectors) {
|
|
335
|
+
console.log(chalk.gray(` ${selector}: ${count} changes`));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const stats = fs.statSync(timelineFile);
|
|
342
|
+
console.log(chalk.gray(`\n File Size: ${formatBytes(stats.size)}`));
|
|
343
|
+
} catch (e) {}
|
|
344
|
+
|
|
345
|
+
console.log();
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case 'export':
|
|
349
|
+
const exportPath = options.output || `chaincss-timeline-${Date.now()}.json`;
|
|
350
|
+
const fullExportPath = path.isAbsolute(exportPath)
|
|
351
|
+
? exportPath
|
|
352
|
+
: path.join(process.cwd(), exportPath);
|
|
353
|
+
|
|
354
|
+
// Add export metadata
|
|
355
|
+
const exportData = {
|
|
356
|
+
...data,
|
|
357
|
+
exportedAt: Date.now(),
|
|
358
|
+
exportedFrom: timelineFile,
|
|
359
|
+
version: '1.0.0'
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
fs.writeFileSync(fullExportPath, JSON.stringify(exportData, null, 2));
|
|
364
|
+
const stats = fs.statSync(fullExportPath);
|
|
365
|
+
console.log(chalk.green(`\nā Timeline exported to ${fullExportPath}`));
|
|
366
|
+
console.log(chalk.gray(` Size: ${formatBytes(stats.size)}`));
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.log(chalk.red(`\nā Failed to export: ${(error as Error).message}`));
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case 'clear':
|
|
373
|
+
try {
|
|
374
|
+
// Create backup before clearing
|
|
375
|
+
const backupPath = `${timelineFile}.backup-${Date.now()}`;
|
|
376
|
+
fs.copyFileSync(timelineFile, backupPath);
|
|
377
|
+
fs.unlinkSync(timelineFile);
|
|
378
|
+
console.log(chalk.green(`\nā Timeline cleared`));
|
|
379
|
+
console.log(chalk.gray(` Backup saved to ${backupPath}`));
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.log(chalk.red(`\nā Failed to clear timeline: ${(error as Error).message}`));
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
|
|
385
|
+
case 'watch':
|
|
386
|
+
console.log(chalk.cyan.bold('\nšļø Watching timeline changes...\n'));
|
|
387
|
+
console.log(chalk.gray(' Press Ctrl+C to stop\n'));
|
|
388
|
+
|
|
389
|
+
let lastModified = fs.statSync(timelineFile).mtimeMs;
|
|
390
|
+
|
|
391
|
+
const watcher = setInterval(() => {
|
|
392
|
+
try {
|
|
393
|
+
const stats = fs.statSync(timelineFile);
|
|
394
|
+
if (stats.mtimeMs > lastModified) {
|
|
395
|
+
lastModified = stats.mtimeMs;
|
|
396
|
+
const newData = loadTimelineData(timelineFile);
|
|
397
|
+
if (newData && newData.history.length !== data.history.length) {
|
|
398
|
+
console.log(chalk.yellow(`\nš Timeline updated - ${new Date().toLocaleTimeString()}`));
|
|
399
|
+
console.log(chalk.gray(` New snapshot count: ${newData.history.length}`));
|
|
400
|
+
|
|
401
|
+
// Show the latest snapshot
|
|
402
|
+
const latest = newData.history[newData.history.length - 1];
|
|
403
|
+
if (latest) {
|
|
404
|
+
console.log(chalk.green(` Latest: ${latest.selector}`));
|
|
405
|
+
console.log(chalk.gray(` Properties: ${Object.keys(latest.styles).length}`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Update data reference
|
|
409
|
+
Object.assign(data, newData);
|
|
410
|
+
}
|
|
411
|
+
} catch (e) {
|
|
412
|
+
// File might be temporarily locked
|
|
413
|
+
}
|
|
414
|
+
}, 1000);
|
|
415
|
+
|
|
416
|
+
// Handle cleanup on exit
|
|
417
|
+
process.on('SIGINT', () => {
|
|
418
|
+
clearInterval(watcher);
|
|
419
|
+
console.log(chalk.gray('\n\nš Stopped watching\n'));
|
|
420
|
+
process.exit(0);
|
|
421
|
+
});
|
|
422
|
+
break;
|
|
423
|
+
|
|
424
|
+
default:
|
|
425
|
+
console.log(chalk.yellow(`\nā Unknown action: ${action}`));
|
|
426
|
+
console.log(chalk.gray('\nAvailable actions:'));
|
|
427
|
+
console.log(chalk.cyan(' list ') + chalk.gray('- List all timeline snapshots'));
|
|
428
|
+
console.log(chalk.cyan(' diff ') + chalk.gray('- Compare two snapshots'));
|
|
429
|
+
console.log(chalk.cyan(' changes ') + chalk.gray('- Show individual style changes'));
|
|
430
|
+
console.log(chalk.cyan(' stats ') + chalk.gray('- Show timeline statistics'));
|
|
431
|
+
console.log(chalk.cyan(' export ') + chalk.gray('- Export timeline to JSON'));
|
|
432
|
+
console.log(chalk.cyan(' clear ') + chalk.gray('- Clear timeline data'));
|
|
433
|
+
console.log(chalk.cyan(' watch ') + chalk.gray('- Watch for timeline updates\n'));
|
|
434
|
+
}
|
|
435
|
+
}
|