Skip to content

Commit 205d0c4

Browse files
committed
feat: CSS Graph Export implemented
1 parent 43040a3 commit 205d0c4

File tree

14 files changed

+1391
-6
lines changed

14 files changed

+1391
-6
lines changed

ice-build/README.md

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Ice Build is a modern, fast, and efficient build tool designed for TypeScript an
1212
* **Watch Mode:** Monitors files for changes and automatically rebuilds.
1313
* **Hot Reloading:** Integrates with `@n8d/ice-hotreloader` for seamless CSS injection and page reloads.
1414
* **PostCSS Integration:** Includes Autoprefixer for CSS vendor prefixes with automatic browserslist configuration discovery.
15+
* **Dependency Visualization:** Exports SCSS dependency graphs in various formats for analysis and visualization.
1516
* **Configurable:** Uses an `ice.config.js` file for project-specific settings.
1617

1718
## Installation
@@ -45,10 +46,18 @@ The following dependencies are bundled with ice-build and don't need separate in
4546
* `--config <path>`: Specify a custom path to the configuration file. Defaults to `./ice.config.js`.
4647
* `--clean`: Clean the output directory before building.
4748
* `--verbose`: Enable verbose logging.
49+
* `--export-graph`: Export CSS dependency graph after build.
50+
* `--graph-format <format>`: Specify graph output format (json, dot, nx, all). Default: json.
51+
* `--graph-output <path>`: Specify output path for graph files.
52+
4853
* **`ice-build watch`**: Starts the build process in watch mode with hot reloading.
4954
* `--config <path>`: Specify a custom path to the configuration file. Defaults to `./ice.config.js`.
5055
* `--verbose`: Enable verbose logging.
5156

57+
* **`ice-build export-graph`**: Export the CSS dependency graph.
58+
* `-f, --format <format>`: Output format (json, dot, nx, all). Default: json.
59+
* `-o, --output <path>`: Output path for the graph files.
60+
5261
> **Backward Compatibility Note:** You can also use `ice-build --watch` for watch mode (equivalent to `ice-build watch`)
5362
>
5463
> **Important:** Make sure the config file path is correct. For example, if your config file is named `ice-build.config.js`, use `--config ./ice-build.config.js`. The path is relative to the current working directory.
@@ -96,6 +105,11 @@ export default {
96105
hotreload: {
97106
port: 8080, // WebSocket server port (default: 8080)
98107
},
108+
// CSS dependency graph export options
109+
graph: {
110+
// format: 'json', // Output format: 'json', 'dot', 'nx', or 'all'
111+
// outputPath: './graphs' // Custom output path for graph files
112+
},
99113
// Copy static assets
100114
assets: {
101115
// Define source and destination for static files
@@ -124,4 +138,89 @@ You can configure your target browsers by adding a browserslist configuration:
124138

125139
2. Or create a `.browserslistrc` file in your project root:
126140
````
127-
`````
141+
142+
## CSS Dependency Graph
143+
144+
Ice Build can generate a visual representation of your SCSS dependencies, helping you understand relationships between your stylesheets.
145+
146+
### Export Formats
147+
148+
- **JSON**: Simple format for programmatic use (`scss-dependency-graph.json`)
149+
- **DOT**: Graphviz format for visual graph representation (`scss-dependency-graph.dot`)
150+
- **NX**: Compatible with NX dependency visualization tools (`scss-dependency-graph-nx.json`)
151+
152+
### Visualizing the Graph
153+
154+
#### Using DOT Graph (Graphviz)
155+
156+
1. Install Graphviz:
157+
```bash
158+
# macOS
159+
brew install graphviz
160+
161+
# Ubuntu/Debian
162+
sudo apt-get install graphviz
163+
164+
# Windows
165+
# Download from https://graphviz.org/download/
166+
```
167+
168+
2. Generate a visual representation:
169+
```bash
170+
# Generate an SVG
171+
dot -Tsvg public/graphs/scss-dependency-graph.dot -o scss-graph.svg
172+
173+
# Generate a PNG
174+
dot -Tpng public/graphs/scss-dependency-graph.dot -o scss-graph.png
175+
```
176+
177+
3. Online alternatives:
178+
- [Graphviz Online](https://dreampuf.github.io/GraphvizOnline/)
179+
- [Viz-js.com](http://viz-js.com/)
180+
- [Edotor.net](https://edotor.net/)
181+
182+
#### Using NX Format
183+
184+
1. Create a simple HTML viewer:
185+
```html
186+
<!DOCTYPE html>
187+
<html>
188+
<head>
189+
<title>SCSS Dependency Graph</title>
190+
<script src="https://unpkg.com/d3@7"></script>
191+
<style>
192+
body { margin: 0; font-family: Arial; }
193+
svg { width: 100vw; height: 100vh; }
194+
</style>
195+
</head>
196+
<body>
197+
<script>
198+
// Load and render the NX graph file
199+
fetch('public/graphs/scss-dependency-graph-nx.json')
200+
.then(response => response.json())
201+
.then(data => {
202+
// Create a simple D3 force-directed graph
203+
// ... See documentation for implementation details
204+
});
205+
</script>
206+
</body>
207+
</html>
208+
```
209+
210+
2. Or use with an NX workspace:
211+
```bash
212+
npx nx graph --file=scss-dependency-graph-nx.json
213+
```
214+
215+
### Example Workflow
216+
217+
```bash
218+
# Export only the graph
219+
npx ice-build export-graph
220+
221+
# Export in multiple formats
222+
npx ice-build export-graph --format all
223+
224+
# Build and export graph in one step
225+
npx ice-build build --export-graph
226+
````

ice-build/src/builders/scss.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export class SCSSBuilder extends EventEmitter implements Builder {
244244
this.dumpDependencyGraph();
245245

246246
logger.success(`Dependency graph built with ${this.dependencyGraph.size} nodes`);
247-
return this.dependencyGraph;
247+
return this.dependencyGraph
248248
} catch (error: unknown) {
249249
const errorMessage = error instanceof Error ? error.message : String(error);
250250
logger.error(`Error building dependency graph: ${errorMessage}`);
@@ -1030,4 +1030,41 @@ export class SCSSBuilder extends EventEmitter implements Builder {
10301030
logger.error(`Error processing SCSS file: ${error instanceof Error ? error.message : String(error)}`);
10311031
}
10321032
}
1033+
1034+
/**
1035+
* Get the SCSS dependency graph
1036+
* @returns A map of file paths to their dependencies
1037+
*/
1038+
public getDependencyGraph(): Record<string, string[]> {
1039+
// Convert the Map<string, SassDependency> to Record<string, string[]>
1040+
const graphRecord: Record<string, string[]> = {};
1041+
1042+
// If the dependency graph exists, convert it
1043+
if (this.dependencyGraph) {
1044+
// Convert Map to Record
1045+
if (this.dependencyGraph instanceof Map) {
1046+
for (const [file, dependency] of this.dependencyGraph.entries()) {
1047+
// Use the 'uses' property which contains what this file imports/depends on
1048+
graphRecord[file] = Array.from(dependency.uses || new Set<string>());
1049+
}
1050+
}
1051+
// Handle if it's already a plain object with a different structure
1052+
else if (typeof this.dependencyGraph === 'object') {
1053+
for (const [file, dependency] of Object.entries(this.dependencyGraph)) {
1054+
// Need to check and handle different potential structures
1055+
// Use type assertion to access properties safely
1056+
const dep = dependency as any;
1057+
if (dep && dep.uses && dep.uses instanceof Set) {
1058+
graphRecord[file] = Array.from(dep.uses);
1059+
} else if (Array.isArray(dep)) {
1060+
graphRecord[file] = dep;
1061+
} else {
1062+
graphRecord[file] = [];
1063+
}
1064+
}
1065+
}
1066+
}
1067+
1068+
return graphRecord;
1069+
}
10331070
}

ice-build/src/cli/commands/build.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ export function registerBuildCommand(program: Command): void {
1010
program
1111
.command('build')
1212
.description('Build all project files')
13-
.action(() => {
14-
executeBuildCommand();
13+
.option('--export-graph', 'Export CSS dependency graph after build')
14+
.option('-f, --graph-format <format>', 'Graph output format (json, dot, nx, all)', 'json')
15+
.option('-o, --graph-output <path>', 'Output path for graph files')
16+
.action((options) => {
17+
executeBuildCommand(options);
1518
});
1619
}
1720

1821
/**
1922
* Execute the build command
2023
*/
21-
async function executeBuildCommand(): Promise<void> {
24+
async function executeBuildCommand(options: {
25+
exportGraph?: boolean,
26+
graphFormat?: string,
27+
graphOutput?: string
28+
} = {}): Promise<void> {
2229
try {
2330
logger.info('Starting build');
2431

@@ -47,12 +54,41 @@ async function executeBuildCommand(): Promise<void> {
4754
config = await configModuleAny.createConfig() || config;
4855
}
4956

57+
// Add graph export options if specified
58+
if (options.exportGraph) {
59+
if (!config.graph) config.graph = {};
60+
config.graph.enabled = true;
61+
}
62+
63+
if (options.graphFormat) {
64+
if (!config.graph) config.graph = {};
65+
config.graph.format = options.graphFormat;
66+
}
67+
68+
if (options.graphOutput) {
69+
if (!config.graph) config.graph = {};
70+
config.graph.outputPath = options.graphOutput;
71+
}
72+
5073
// Create builder manager - only pass the config, not the output path
5174
const buildManager = new BuildManagerModule.Builder(config as any);
5275

5376
// Build all
5477
await buildManager.buildAll();
5578

79+
// Export graph if requested
80+
if (options.exportGraph || config.graph?.enabled) {
81+
logger.info('Exporting CSS dependency graph');
82+
const { exportGraph } = await import('../../exporters/graph-exporter.js');
83+
84+
// No need to call buildDependencyGraph() here since buildAll() should have already built it,
85+
// but let's make sure the graph is ready
86+
const scssBuilder = buildManager.getScssBuilder();
87+
88+
// Export the graph
89+
await exportGraph(scssBuilder, config);
90+
}
91+
5692
logger.success('Build complete');
5793
} catch (error: unknown) {
5894
const errorMessage = error instanceof Error ? error.message : String(error);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Command } from 'commander';
2+
import { Logger } from '../../utils/logger.js';
3+
4+
const logger = new Logger('Graph Export');
5+
6+
/**
7+
* Register the export graph command
8+
*/
9+
export function registerExportGraphCommand(program: Command): void {
10+
program
11+
.command('export-graph')
12+
.description('Export CSS dependency graph')
13+
.option('-f, --format <format>', 'Output format (json, dot, nx, all)', 'json')
14+
.option('-o, --output <path>', 'Output path for the graph files')
15+
.action((options) => {
16+
executeExportGraphCommand(options);
17+
});
18+
}
19+
20+
/**
21+
* Execute the export graph command
22+
*/
23+
async function executeExportGraphCommand(options: { format?: string, output?: string }): Promise<void> {
24+
try {
25+
logger.info('Starting CSS dependency graph export');
26+
27+
// Dynamically import modules
28+
const configModule = await import('../../config/index.js');
29+
const BuildManagerModule = await import('../../builders/index.js');
30+
const { exportGraph } = await import('../../exporters/graph-exporter.js');
31+
32+
// Get configuration
33+
let config: any = {
34+
input: {
35+
scss: ['src/**/*.scss', 'source/**/*.scss'],
36+
},
37+
output: {
38+
path: 'public'
39+
}
40+
};
41+
42+
const configModuleAny = configModule as any;
43+
44+
if (typeof configModuleAny.getConfig === 'function') {
45+
config = await configModuleAny.getConfig() || config;
46+
} else if (typeof configModuleAny.createConfig === 'function') {
47+
config = await configModuleAny.createConfig() || config;
48+
}
49+
50+
// Override config with command line options
51+
if (options.format) {
52+
if (!config.graph) config.graph = {};
53+
config.graph.format = options.format;
54+
}
55+
56+
if (options.output) {
57+
if (!config.graph) config.graph = {};
58+
config.graph.outputPath = options.output;
59+
}
60+
61+
// Create builder manager
62+
const buildManager = new BuildManagerModule.Builder(config as any);
63+
64+
// Get the SCSS builder
65+
const scssBuilder = buildManager.getScssBuilder();
66+
67+
// IMPORTANT: Build the dependency graph before exporting
68+
logger.info('Building SCSS dependency graph...');
69+
await scssBuilder.buildDependencyGraph();
70+
71+
// Export the graph
72+
await exportGraph(scssBuilder, config);
73+
74+
logger.success('Graph export complete');
75+
} catch (error: unknown) {
76+
const errorMessage = error instanceof Error ? error.message : String(error);
77+
logger.error(`Graph export failed: ${errorMessage}`);
78+
process.exit(1);
79+
}
80+
}

ice-build/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Command } from 'commander';
22
import { registerBuildCommand } from './commands/build.js';
33
import { registerWatchCommand } from './commands/watch.js';
44
import { checkDirectories } from './commands/check-dirs.js';
5+
import { registerExportGraphCommand } from './commands/export-graph.js';
56

67
/**
78
* Create and configure the CLI program
@@ -19,6 +20,7 @@ export function createCLI(): Command {
1920
registerBuildCommand(program);
2021
registerWatchCommand(program);
2122
registerCheckCommand(program);
23+
registerExportGraphCommand(program);
2224

2325
return program;
2426
}

ice-build/src/config/defaults.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ export const defaultConfig: Partial<IceConfig> = {
1818
port: 3001, // Ensure port is correctly defined as 3001
1919
host: 'localhost',
2020
debounceTime: 300
21+
},
22+
graph: {
23+
format: 'json'
2124
}
2225
};

0 commit comments

Comments
 (0)