ngx-devkit-builders 1.3.3 → 2.0.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 - 2024 Celtian
3
+ Copyright (c) 2022 - 2026 Celtian
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  This package contains Architect builders used to build and test Angular applications and libraries.
15
15
 
16
- > ✓ _Angular 18 compatible_
16
+ > ✓ _Angular 21 compatible_
17
17
 
18
18
  ## 🚀 Builders
19
19
 
@@ -22,6 +22,7 @@ This package contains Architect builders used to build and test Angular applicat
22
22
  | [version](./src/version/README.md) | Create build information file |
23
23
  | [robots](./src/robots/README.md) | Create simplified robots.txt file |
24
24
  | [copy-environment](./src/copy-environment/README.md) | Copy environment file |
25
+ | [sort-imports](./src/sort-imports/README.md) | Sort imports of components |
25
26
 
26
27
  _More builders can be added in the future._
27
28
 
@@ -29,12 +30,13 @@ _More builders can be added in the future._
29
30
 
30
31
  | Angular | ngx-devkit-builders | Install |
31
32
  | ------- | ------------------- | -------------------------------- |
32
- | >= 17 | 1.x | `yarn add ngx-devkit-builders` |
33
+ | >= 21 | 2.x | `yarn add ngx-devkit-builders` |
34
+ | >= 17 | 1.x | `yarn add ngx-devkit-builders@1` |
33
35
  | >= 16 | 0.x | `yarn add ngx-devkit-builders@0` |
34
36
 
35
37
  ## 🪪 License
36
38
 
37
- Copyright © 2022 - 2024 [Dominik Hladik](https://github.com/Celtian)
39
+ Copyright © 2022 - 2026 [Dominik Hladik](https://github.com/Celtian)
38
40
 
39
41
  All contents are licensed under the [MIT license].
40
42
 
package/builders.json CHANGED
@@ -14,6 +14,11 @@
14
14
  "description": "Create build information file",
15
15
  "implementation": "./version",
16
16
  "schema": "./version/schema.json"
17
+ },
18
+ "sort-imports": {
19
+ "description": "Sorts imports in Angular components and directives using AST analysis",
20
+ "implementation": "./dist/sort-imports",
21
+ "schema": "./dist/sort-imports/schema.json"
17
22
  }
18
23
  }
19
24
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "source": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-devkit-builders",
3
- "version": "1.3.3",
3
+ "version": "2.0.0",
4
4
  "author": {
5
5
  "name": "Dominik Hladík",
6
6
  "email": "dominik.hladik@seznam.cz",
@@ -12,9 +12,10 @@
12
12
  "builders": "builders.json",
13
13
  "scripts": {},
14
14
  "dependencies": {
15
- "@angular-devkit/architect": "^0.1802.9",
16
- "@angular-devkit/core": "^18.2.9",
17
- "fs-extra": "^11.2.0"
15
+ "@angular-devkit/architect": "^0.2100.4",
16
+ "@angular-devkit/core": "^21.0.4",
17
+ "@schematics/angular": "^21.0.4",
18
+ "fs-extra": "^11.3.3"
18
19
  },
19
20
  "devDependencies": {},
20
21
  "homepage": "https://github.com/Celtian/ngx-devkit-builders#readme",
@@ -32,14 +33,14 @@
32
33
  "url": "https://github.com/Celtian/ngx-devkit-builders/issues"
33
34
  },
34
35
  "engines": {
35
- "node": ">=12",
36
+ "node": ">=20",
36
37
  "npm": "please-use-yarn"
37
38
  },
38
39
  "publishConfig": {
39
40
  "registry": "https://registry.npmjs.org"
40
41
  },
41
42
  "peerDependencies": {
42
- "@angular/core": ">=12",
43
- "@angular/cli": ">=12"
43
+ "@angular/core": ">=21",
44
+ "@angular/cli": ">=21"
44
45
  }
45
46
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "allow": {
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const architect_1 = require("@angular-devkit/architect");
4
+ const ast_utils_1 = require("@schematics/angular/utility/ast-utils");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const ts = require("typescript");
8
+ // ANSI color codes for terminal output
9
+ const colors = {
10
+ reset: '\x1b[0m',
11
+ bright: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ red: '\x1b[31m',
14
+ green: '\x1b[32m',
15
+ yellow: '\x1b[33m',
16
+ blue: '\x1b[34m',
17
+ magenta: '\x1b[35m',
18
+ cyan: '\x1b[36m',
19
+ white: '\x1b[37m',
20
+ gray: '\x1b[90m',
21
+ };
22
+ const walkDirectory = (dir, files = []) => {
23
+ const items = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
24
+ for (const item of items) {
25
+ const fullPath = (0, path_1.join)(dir, item.name);
26
+ if (item.isDirectory()) {
27
+ walkDirectory(fullPath, files);
28
+ }
29
+ else if (item.isFile() &&
30
+ item.name.endsWith('.ts') &&
31
+ !item.name.endsWith('.spec.ts') &&
32
+ !item.name.endsWith('.d.ts')) {
33
+ files.push(fullPath);
34
+ }
35
+ }
36
+ return files;
37
+ };
38
+ const isAngularComponentOrDirective = (filePath, includeDirectives) => {
39
+ try {
40
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
41
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
42
+ const componentDecorators = (0, ast_utils_1.getDecoratorMetadata)(sourceFile, 'Component', '@angular/core');
43
+ if (componentDecorators.length > 0) {
44
+ return true;
45
+ }
46
+ if (includeDirectives) {
47
+ const directiveDecorators = (0, ast_utils_1.getDecoratorMetadata)(sourceFile, 'Directive', '@angular/core');
48
+ return directiveDecorators.length > 0;
49
+ }
50
+ return false;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ };
56
+ const analyzeComponentImports = (sourceFile) => {
57
+ const componentImports = [];
58
+ const visitNode = (node) => {
59
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
60
+ const callExpr = node.expression;
61
+ // Check if this is a @Component decorator
62
+ if (ts.isIdentifier(callExpr.expression) && callExpr.expression.text === 'Component') {
63
+ if (callExpr.arguments.length > 0) {
64
+ const arg = callExpr.arguments[0];
65
+ if (ts.isObjectLiteralExpression(arg)) {
66
+ // Look for the imports property
67
+ for (const property of arg.properties) {
68
+ if (ts.isPropertyAssignment(property) &&
69
+ ts.isIdentifier(property.name) &&
70
+ property.name.text === 'imports') {
71
+ if (ts.isArrayLiteralExpression(property.initializer)) {
72
+ // Extract import names from the array
73
+ for (const element of property.initializer.elements) {
74
+ if (ts.isIdentifier(element)) {
75
+ componentImports.push(element.text);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ts.forEachChild(node, visitNode);
86
+ };
87
+ visitNode(sourceFile);
88
+ return componentImports;
89
+ };
90
+ const sortComponentImports = (imports) => {
91
+ // Sort imports alphabetically
92
+ return [...imports].sort();
93
+ };
94
+ const processFile = (filePath, options) => {
95
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
96
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
97
+ const componentName = (0, path_1.basename)(filePath, '.ts');
98
+ const importsArray = analyzeComponentImports(sourceFile);
99
+ const sortedImports = sortComponentImports(importsArray);
100
+ // Check if imports need to be reordered
101
+ const hasChanges = JSON.stringify(importsArray) !== JSON.stringify(sortedImports);
102
+ if (hasChanges && !options.dryRun) {
103
+ // Find and replace the imports array in the component decorator
104
+ const updatedContent = updateComponentImports(content, sortedImports);
105
+ (0, fs_1.writeFileSync)(filePath, updatedContent, 'utf8');
106
+ }
107
+ return {
108
+ filePath,
109
+ componentName,
110
+ importsArray,
111
+ sortedImports,
112
+ hasChanges,
113
+ };
114
+ };
115
+ const updateComponentImports = (content, sortedImports) => {
116
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
117
+ let updatedContent = content;
118
+ const visitNode = (node) => {
119
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
120
+ const callExpr = node.expression;
121
+ if (ts.isIdentifier(callExpr.expression) && callExpr.expression.text === 'Component') {
122
+ if (callExpr.arguments.length > 0) {
123
+ const arg = callExpr.arguments[0];
124
+ if (ts.isObjectLiteralExpression(arg)) {
125
+ for (const property of arg.properties) {
126
+ if (ts.isPropertyAssignment(property) &&
127
+ ts.isIdentifier(property.name) &&
128
+ property.name.text === 'imports') {
129
+ if (ts.isArrayLiteralExpression(property.initializer)) {
130
+ // Replace the array content
131
+ const start = property.initializer.getStart(sourceFile);
132
+ const end = property.initializer.getEnd();
133
+ const newImportsArray = `[${sortedImports.join(', ')}]`;
134
+ updatedContent = content.substring(0, start) + newImportsArray + content.substring(end);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ ts.forEachChild(node, visitNode);
143
+ };
144
+ visitNode(sourceFile);
145
+ return updatedContent;
146
+ };
147
+ const outputResults = (analyses, options) => {
148
+ const changedFiles = analyses.filter((a) => a.hasChanges);
149
+ const unchangedFiles = analyses.filter((a) => !a.hasChanges);
150
+ console.log('\n📋 COMPONENT IMPORTS SORTING ANALYSIS');
151
+ console.log('━'.repeat(80));
152
+ if (options.dryRun) {
153
+ console.log('🔍 DRY RUN MODE - No files were modified');
154
+ console.log('');
155
+ }
156
+ console.log(`📊 Summary: ${analyses.length} files analyzed`);
157
+ console.log(`✅ ${unchangedFiles.length} files already have sorted imports`);
158
+ console.log(`🔄 ${changedFiles.length} files ${options.dryRun ? 'would be' : 'were'} updated`);
159
+ if (options.verbose && changedFiles.length > 0) {
160
+ console.log('');
161
+ console.log(`📝 Files ${options.dryRun ? 'that would be' : 'that were'} updated:`);
162
+ console.log('─'.repeat(80));
163
+ console.log('Component Name'.padEnd(35) + 'Sorted Imports');
164
+ console.log('─'.repeat(80));
165
+ changedFiles.forEach((analysis) => {
166
+ const componentName = analysis.componentName.length > 32 ? analysis.componentName.substring(0, 29) + '...' : analysis.componentName;
167
+ const sortedImports = `[${analysis.sortedImports.join(', ')}]`;
168
+ // Use cyan color for sorted imports to match the existing imports color
169
+ console.log(componentName.padEnd(35) + colors.cyan + sortedImports + colors.reset);
170
+ });
171
+ console.log('─'.repeat(80));
172
+ }
173
+ if (options.verbose && unchangedFiles.length > 0) {
174
+ console.log('');
175
+ console.log('✨ Files with already sorted imports:');
176
+ console.log('─'.repeat(80));
177
+ console.log('Component Name'.padEnd(35) + 'Current Imports');
178
+ console.log('─'.repeat(80));
179
+ unchangedFiles.forEach((analysis) => {
180
+ const componentName = analysis.componentName.length > 32 ? analysis.componentName.substring(0, 29) + '...' : analysis.componentName;
181
+ if (analysis.importsArray.length > 0) {
182
+ const imports = `[${analysis.importsArray.join(', ')}]`;
183
+ // Use cyan color for components with existing sorted imports
184
+ console.log(componentName.padEnd(35) + colors.cyan + imports + colors.reset);
185
+ }
186
+ else {
187
+ // Use gray/dim color for components without imports
188
+ const noImportsText = 'No imports array found';
189
+ console.log(componentName.padEnd(35) + colors.gray + colors.dim + noImportsText + colors.reset);
190
+ }
191
+ });
192
+ console.log('─'.repeat(80));
193
+ }
194
+ console.log('━'.repeat(80));
195
+ };
196
+ exports.default = (0, architect_1.createBuilder)((options, context) => {
197
+ return new Promise((resolve) => {
198
+ try {
199
+ const projectRoot = context.target?.project
200
+ ? (0, path_1.join)(context.workspaceRoot, 'projects', context.target.project)
201
+ : context.workspaceRoot;
202
+ const srcDir = (0, path_1.join)(projectRoot, 'src');
203
+ if (!(0, fs_1.statSync)(srcDir).isDirectory()) {
204
+ context.logger.error(`Source directory not found: ${srcDir}`);
205
+ resolve({ success: false });
206
+ return;
207
+ }
208
+ // Find all TypeScript files recursively
209
+ const allTsFiles = walkDirectory(srcDir);
210
+ // Filter to only component and directive files
211
+ const targetFiles = allTsFiles.filter((file) => isAngularComponentOrDirective(file, options.includeDirectives));
212
+ if (targetFiles.length === 0) {
213
+ console.log('No Angular components or directives found.');
214
+ resolve({ success: true });
215
+ return;
216
+ }
217
+ // Process each file
218
+ const analyses = [];
219
+ for (const filePath of targetFiles) {
220
+ try {
221
+ const analysis = processFile(filePath, options);
222
+ analyses.push(analysis);
223
+ }
224
+ catch (error) {
225
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
226
+ context.logger.warn(`Failed to process ${filePath}: ${errorMessage}`);
227
+ }
228
+ }
229
+ // Output results
230
+ outputResults(analyses, options);
231
+ resolve({ success: true });
232
+ }
233
+ catch (err) {
234
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
235
+ context.logger.error('Error sorting imports: ' + errorMessage);
236
+ resolve({ success: false });
237
+ }
238
+ });
239
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "dryRun": {
6
+ "description": "Run the builder without making any changes to files",
7
+ "type": "boolean",
8
+ "default": false
9
+ },
10
+ "verbose": {
11
+ "description": "Extended logging output",
12
+ "type": "boolean",
13
+ "default": false
14
+ },
15
+ "includeDirectives": {
16
+ "description": "Include Angular directives in addition to components",
17
+ "type": "boolean",
18
+ "default": true
19
+ }
20
+ }
21
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "http://json-schema.org/schema",
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "type": "object",
4
4
  "properties": {
5
5
  "outputFile": {