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 +1 -1
- package/README.md +5 -3
- package/builders.json +5 -0
- package/copy-environment/schema.json +1 -1
- package/package.json +8 -7
- package/robots/schema.json +1 -1
- package/sort-imports/index.js +239 -0
- package/sort-imports/schema.json +21 -0
- package/version/schema.json +1 -1
package/LICENSE
CHANGED
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
|
|
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
|
-
| >=
|
|
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 -
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-devkit-builders",
|
|
3
|
-
"version": "
|
|
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.
|
|
16
|
-
"@angular-devkit/core": "^
|
|
17
|
-
"
|
|
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": ">=
|
|
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": ">=
|
|
43
|
-
"@angular/cli": ">=
|
|
43
|
+
"@angular/core": ">=21",
|
|
44
|
+
"@angular/cli": ">=21"
|
|
44
45
|
}
|
|
45
46
|
}
|
package/robots/schema.json
CHANGED
|
@@ -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
|
+
}
|
package/version/schema.json
CHANGED