ngx-devkit-builders 1.3.3 → 2.0.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 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
  }
@@ -3,16 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const architect_1 = require("@angular-devkit/architect");
4
4
  const core_1 = require("@angular-devkit/core");
5
5
  const fs_extra_1 = require("fs-extra");
6
- async function copyFiles({ sourceFile, targetFile, overwrite }) {
7
- return new Promise((resolve, reject) => {
8
- (0, fs_extra_1.copy)(sourceFile, targetFile, { overwrite }, () => {
9
- reject();
10
- });
11
- resolve();
12
- });
13
- }
14
6
  exports.default = (0, architect_1.createBuilder)(async ({ verbose, source, target, overwrite }, ctx) => {
15
- ctx.logger.info('🚧 Creating robots file…');
7
+ ctx.logger.info('🚧 Copying environment…');
16
8
  const projectMetadata = await ctx.getProjectMetadata(ctx.target.project);
17
9
  if (projectMetadata.projectType !== 'application') {
18
10
  ctx.logger.error('❌ Project must be type of application');
@@ -30,11 +22,7 @@ exports.default = (0, architect_1.createBuilder)(async ({ verbose, source, targe
30
22
  const sourceFile = `${environmentsFolder}/${source}`;
31
23
  const targetFile = `${environmentsFolder}/${target}`;
32
24
  try {
33
- await copyFiles({
34
- sourceFile,
35
- targetFile,
36
- overwrite,
37
- });
25
+ await (0, fs_extra_1.copy)(sourceFile, targetFile, { overwrite });
38
26
  if (overwrite) {
39
27
  ctx.logger.info(`✔️ Environment replaced in "${targetFile}"`);
40
28
  }
@@ -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.1",
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,215 @@
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
+ const walkDirectory = (dir, files = []) => {
9
+ const items = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
10
+ for (const item of items) {
11
+ const fullPath = (0, path_1.join)(dir, item.name);
12
+ if (item.isDirectory()) {
13
+ walkDirectory(fullPath, files);
14
+ }
15
+ else if (item.isFile() &&
16
+ item.name.endsWith('.ts') &&
17
+ !item.name.endsWith('.spec.ts') &&
18
+ !item.name.endsWith('.d.ts')) {
19
+ files.push(fullPath);
20
+ }
21
+ }
22
+ return files;
23
+ };
24
+ const isAngularComponentOrDirective = (filePath, includeDirectives) => {
25
+ try {
26
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
27
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
28
+ const componentDecorators = (0, ast_utils_1.getDecoratorMetadata)(sourceFile, 'Component', '@angular/core');
29
+ if (componentDecorators.length > 0) {
30
+ return true;
31
+ }
32
+ if (includeDirectives) {
33
+ const directiveDecorators = (0, ast_utils_1.getDecoratorMetadata)(sourceFile, 'Directive', '@angular/core');
34
+ return directiveDecorators.length > 0;
35
+ }
36
+ return false;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ };
42
+ const analyzeComponentImports = (sourceFile) => {
43
+ const componentImports = [];
44
+ const visitNode = (node) => {
45
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
46
+ const callExpr = node.expression;
47
+ // Check if this is a @Component decorator
48
+ if (ts.isIdentifier(callExpr.expression) && callExpr.expression.text === 'Component') {
49
+ if (callExpr.arguments.length > 0) {
50
+ const arg = callExpr.arguments[0];
51
+ if (ts.isObjectLiteralExpression(arg)) {
52
+ // Look for the imports property
53
+ for (const property of arg.properties) {
54
+ if (ts.isPropertyAssignment(property) &&
55
+ ts.isIdentifier(property.name) &&
56
+ property.name.text === 'imports') {
57
+ if (ts.isArrayLiteralExpression(property.initializer)) {
58
+ // Extract import names from the array
59
+ for (const element of property.initializer.elements) {
60
+ if (ts.isIdentifier(element)) {
61
+ componentImports.push(element.text);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ts.forEachChild(node, visitNode);
72
+ };
73
+ visitNode(sourceFile);
74
+ return componentImports;
75
+ };
76
+ const sortComponentImports = (imports) => {
77
+ // Sort imports alphabetically
78
+ return [...imports].sort();
79
+ };
80
+ const processFile = (filePath, options) => {
81
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
82
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
83
+ const componentName = (0, path_1.basename)(filePath, '.ts');
84
+ const importsArray = analyzeComponentImports(sourceFile);
85
+ const sortedImports = sortComponentImports(importsArray);
86
+ // Check if imports need to be reordered
87
+ const hasChanges = JSON.stringify(importsArray) !== JSON.stringify(sortedImports);
88
+ if (hasChanges && !options.dryRun) {
89
+ // Find and replace the imports array in the component decorator
90
+ const updatedContent = updateComponentImports(content, sortedImports);
91
+ (0, fs_1.writeFileSync)(filePath, updatedContent, 'utf8');
92
+ }
93
+ return {
94
+ filePath,
95
+ componentName,
96
+ importsArray,
97
+ sortedImports,
98
+ hasChanges,
99
+ };
100
+ };
101
+ const updateComponentImports = (content, sortedImports) => {
102
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
103
+ let updatedContent = content;
104
+ const visitNode = (node) => {
105
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
106
+ const callExpr = node.expression;
107
+ if (ts.isIdentifier(callExpr.expression) && callExpr.expression.text === 'Component') {
108
+ if (callExpr.arguments.length > 0) {
109
+ const arg = callExpr.arguments[0];
110
+ if (ts.isObjectLiteralExpression(arg)) {
111
+ for (const property of arg.properties) {
112
+ if (ts.isPropertyAssignment(property) &&
113
+ ts.isIdentifier(property.name) &&
114
+ property.name.text === 'imports') {
115
+ if (ts.isArrayLiteralExpression(property.initializer)) {
116
+ // Replace the array content
117
+ const start = property.initializer.getStart(sourceFile);
118
+ const end = property.initializer.getEnd();
119
+ const newImportsArray = `[${sortedImports.join(', ')}]`;
120
+ updatedContent = content.substring(0, start) + newImportsArray + content.substring(end);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ ts.forEachChild(node, visitNode);
129
+ };
130
+ visitNode(sourceFile);
131
+ return updatedContent;
132
+ };
133
+ const outputResults = (analyses, options, context) => {
134
+ const changedFiles = analyses.filter((a) => a.hasChanges);
135
+ const unchangedFiles = analyses.filter((a) => !a.hasChanges);
136
+ context.logger.info('\n📋 COMPONENT IMPORTS SORTING ANALYSIS');
137
+ context.logger.info('━'.repeat(80));
138
+ if (options.dryRun) {
139
+ context.logger.info('🔍 DRY RUN MODE - No files were modified');
140
+ }
141
+ context.logger.info(`📊 Summary: ${analyses.length} files analyzed`);
142
+ context.logger.info(`✅ ${unchangedFiles.length} files already have sorted imports`);
143
+ context.logger.info(`🔄 ${changedFiles.length} files ${options.dryRun ? 'would be' : 'were'} updated`);
144
+ if (options.verbose && changedFiles.length > 0) {
145
+ context.logger.info(`📝 Files ${options.dryRun ? 'that would be' : 'that were'} updated:`);
146
+ context.logger.info('─'.repeat(80));
147
+ context.logger.info('Component Name'.padEnd(35) + 'Sorted Imports');
148
+ context.logger.info('─'.repeat(80));
149
+ changedFiles.forEach((analysis) => {
150
+ const componentName = analysis.componentName.length > 32 ? analysis.componentName.substring(0, 29) + '...' : analysis.componentName;
151
+ const sortedImports = `[${analysis.sortedImports.join(', ')}]`;
152
+ context.logger.info(componentName.padEnd(35) + sortedImports);
153
+ });
154
+ context.logger.info('─'.repeat(80));
155
+ }
156
+ if (options.verbose && unchangedFiles.length > 0) {
157
+ context.logger.info('✨ Files with already sorted imports:');
158
+ context.logger.info('─'.repeat(80));
159
+ context.logger.info('Component Name'.padEnd(35) + 'Current Imports');
160
+ context.logger.info('─'.repeat(80));
161
+ unchangedFiles.forEach((analysis) => {
162
+ const componentName = analysis.componentName.length > 32 ? analysis.componentName.substring(0, 29) + '...' : analysis.componentName;
163
+ if (analysis.importsArray.length > 0) {
164
+ const imports = `[${analysis.importsArray.join(', ')}]`;
165
+ context.logger.info(componentName.padEnd(35) + imports);
166
+ }
167
+ else {
168
+ const noImportsText = 'No imports array found';
169
+ context.logger.info(componentName.padEnd(35) + noImportsText);
170
+ }
171
+ });
172
+ context.logger.info('─'.repeat(80));
173
+ }
174
+ context.logger.info('━'.repeat(80));
175
+ };
176
+ exports.default = (0, architect_1.createBuilder)(async (options, context) => {
177
+ try {
178
+ const projectRoot = context.target?.project
179
+ ? (0, path_1.join)(context.workspaceRoot, 'projects', context.target.project)
180
+ : context.workspaceRoot;
181
+ const srcDir = (0, path_1.join)(projectRoot, 'src');
182
+ if (!(0, fs_1.statSync)(srcDir).isDirectory()) {
183
+ context.logger.error(`Source directory not found: ${srcDir}`);
184
+ return { success: false };
185
+ }
186
+ // Find all TypeScript files recursively
187
+ const allTsFiles = walkDirectory(srcDir);
188
+ // Filter to only component and directive files
189
+ const targetFiles = allTsFiles.filter((file) => isAngularComponentOrDirective(file, options.includeDirectives));
190
+ if (targetFiles.length === 0) {
191
+ context.logger.info('No Angular components or directives found.');
192
+ return { success: true };
193
+ }
194
+ // Process each file
195
+ const analyses = [];
196
+ for (const filePath of targetFiles) {
197
+ try {
198
+ const analysis = processFile(filePath, options);
199
+ analyses.push(analysis);
200
+ }
201
+ catch (error) {
202
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
203
+ context.logger.warn(`Failed to process ${filePath}: ${errorMessage}`);
204
+ }
205
+ }
206
+ // Output results
207
+ outputResults(analyses, options, context);
208
+ return { success: true };
209
+ }
210
+ catch (err) {
211
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
212
+ context.logger.error('Error sorting imports: ' + errorMessage);
213
+ return { success: false };
214
+ }
215
+ });
@@ -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": {