@craft-ng/dev-tools 0.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/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # @craft-ng/dev-tools
2
+
3
+ Development tools for ng-craft: ESLint configs, ESLint rules, and codemods.
4
+
5
+ ## Installation
6
+
7
+ ### Depuis npm (après publication)
8
+
9
+ ```bash
10
+ npm install -D @craft-ng/dev-tools
11
+ ```
12
+
13
+ ### Publication sur npm
14
+
15
+ Pour publier le package sur npm (mainteneurs uniquement) :
16
+
17
+ #### 1. Se connecter à npm
18
+
19
+ ```bash
20
+ npm login
21
+ ```
22
+
23
+ Vous aurez besoin d'un compte npm avec les droits de publication sur le scope `@craft-ng`.
24
+
25
+ #### 2. Builder le package
26
+
27
+ ```bash
28
+ npx nx build dev-tools
29
+ ```
30
+
31
+ #### 3. Mettre à jour la version
32
+
33
+ Modifiez la version dans `libs/dev-tools/package.json` selon [semver](https://semver.org/) :
34
+
35
+ - **Patch** (0.1.0 → 0.1.1) : Corrections de bugs
36
+ - **Minor** (0.1.0 → 0.2.0) : Nouvelles fonctionnalités rétrocompatibles
37
+ - **Major** (0.1.0 → 1.0.0) : Changements non rétrocompatibles
38
+
39
+ #### 4. Publier
40
+
41
+ Depuis le répertoire de build :
42
+
43
+ ```bash
44
+ cd dist/libs/dev-tools
45
+ npm publish --access public
46
+ ```
47
+
48
+ > **Note** : L'option `--access public` est nécessaire pour les packages scoped (`@craft-ng/...`) si vous souhaitez les rendre publics.
49
+
50
+ #### 5. Créer un tag Git (recommandé)
51
+
52
+ Retournez à la racine et créez un tag :
53
+
54
+ ```bash
55
+ cd ../../..
56
+ git tag dev-tools-v0.1.0
57
+ git push origin dev-tools-v0.1.0
58
+ ```
59
+
60
+ ### En développement local (avant publication)
61
+
62
+ Pour tester ce package dans un autre projet avant de le publier sur npm :
63
+
64
+ #### 1. Builder le package
65
+
66
+ Depuis la racine du monorepo ng-craft :
67
+
68
+ ```bash
69
+ npx nx build dev-tools
70
+ ```
71
+
72
+ Cela génère les fichiers compilés dans `dist/libs/dev-tools/`.
73
+
74
+ #### 2. Créer un lien npm local
75
+
76
+ Depuis la racine du monorepo :
77
+
78
+ ```bash
79
+ cd dist/libs/dev-tools
80
+ npm link
81
+ ```
82
+
83
+ #### 3. Utiliser le lien dans votre autre projet
84
+
85
+ Dans votre projet Angular externe :
86
+
87
+ ```bash
88
+ cd /chemin/vers/votre-projet
89
+ npm link @craft-ng/dev-tools
90
+ ```
91
+
92
+ > **Note** : Après chaque modification du code de `@craft-ng/dev-tools`, vous devez rebuilder avec `npx nx build dev-tools` pour que les changements soient pris en compte dans les projets liés.
93
+
94
+ #### 4. Supprimer le lien (quand terminé)
95
+
96
+ Dans votre projet externe :
97
+
98
+ ```bash
99
+ npm unlink @craft-ng/dev-tools
100
+ npm install
101
+ ```
102
+
103
+ ## Configuration complète dans un nouveau projet
104
+
105
+ Voici un exemple de configuration complète pour utiliser tous les outils dans un projet Angular :
106
+
107
+ ### 1. Installer les dépendances
108
+
109
+ ```bash
110
+ npm install -D @craft-ng/dev-tools @craft-ng/core
111
+ npm install -D @angular-eslint/eslint-plugin @typescript-eslint/eslint-plugin eslint
112
+ ```
113
+
114
+ ### 2. Configurer ESLint
115
+
116
+ Créez ou modifiez `eslint.config.mjs` :
117
+
118
+ ```javascript
119
+ import craftAngular from '@craft-ng/dev-tools/eslint/angular';
120
+ import craftRules from '@craft-ng/dev-tools/eslint-rules';
121
+
122
+ export default [
123
+ {
124
+ ignores: ['dist/**', 'node_modules/**'],
125
+ },
126
+ ...craftAngular,
127
+ {
128
+ files: ['**/*.ts'],
129
+ plugins: {
130
+ '@craft-ng': craftRules,
131
+ },
132
+ rules: {
133
+ // Règles craft-ng recommandées
134
+ '@craft-ng/brand-angular-deps-match': 'error',
135
+ '@craft-ng/no-angular-inject': 'error',
136
+ '@craft-ng/no-direct-angular-class-export': 'error',
137
+ },
138
+ },
139
+ ];
140
+ ```
141
+
142
+ ### 3. Transformer votre code existant
143
+
144
+ ```bash
145
+ # Prévisualiser les changements (dry-run)
146
+ npx craft-brand --root ./src --dry-run
147
+
148
+ # Appliquer les transformations
149
+ npx craft-brand --root ./src
150
+ ```
151
+
152
+ ### 4. Ajouter des scripts npm
153
+
154
+ Dans votre `package.json` :
155
+
156
+ ```json
157
+ {
158
+ "scripts": {
159
+ "lint": "eslint .",
160
+ "lint:fix": "eslint . --fix",
161
+ "codemod:preview": "craft-brand --dry-run",
162
+ "codemod:apply": "craft-brand"
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## Features
168
+
169
+ ### 1. ESLint Configurations
170
+
171
+ Pre-configured ESLint settings for ng-craft projects.
172
+
173
+ #### Basic TypeScript Config
174
+
175
+ ```javascript
176
+ // eslint.config.mjs
177
+ import craftEslint from '@craft-ng/dev-tools/eslint';
178
+
179
+ export default [...craftEslint];
180
+ ```
181
+
182
+ #### Angular Config
183
+
184
+ ```javascript
185
+ // eslint.config.mjs
186
+ import craftAngular from '@craft-ng/dev-tools/eslint/angular';
187
+
188
+ export default [...craftAngular];
189
+ ```
190
+
191
+ ### 2. ESLint Rules
192
+
193
+ Custom ESLint rules for enforcing ng-craft patterns.
194
+
195
+ #### Available Rules
196
+
197
+ - **`brand-angular-deps-match`** - Ensures `deps({ injected, importDeps, providers })` matches the Angular symbol dependencies
198
+ - **`no-angular-inject`** - Disallows Angular `inject()`, `@Injectable`, and `@Service` usage
199
+ - **`no-direct-angular-class-export`** - Disallows direct exports of Angular symbols managed through `brandAngularSymbol`
200
+
201
+ #### Usage
202
+
203
+ ```javascript
204
+ // eslint.config.mjs
205
+ import craftAngular from '@craft-ng/dev-tools/eslint/angular';
206
+ import craftRules from '@craft-ng/dev-tools/eslint-rules';
207
+
208
+ export default [
209
+ ...craftAngular,
210
+ {
211
+ plugins: {
212
+ '@craft-ng': craftRules,
213
+ },
214
+ rules: {
215
+ '@craft-ng/brand-angular-deps-match': 'error',
216
+ '@craft-ng/no-angular-inject': 'error',
217
+ '@craft-ng/no-direct-angular-class-export': 'error',
218
+ },
219
+ },
220
+ ];
221
+ ```
222
+
223
+ ### 3. Codemod: `craft-brand`
224
+
225
+ Automatically transforms Angular components, directives, pipes, and injectables to use `brandAngularSymbol` and `deps()`.
226
+
227
+ #### CLI Usage
228
+
229
+ ```bash
230
+ # Transform all TypeScript files in the current directory
231
+ npx craft-brand
232
+
233
+ # Transform with specific root directory
234
+ npx craft-brand --root ./src
235
+
236
+ # Dry run (preview changes without writing)
237
+ npx craft-brand --dry-run
238
+
239
+ # Only transform standalone components/directives
240
+ npx craft-brand --transform-only-standalone-declarables
241
+
242
+ # Custom helper import path
243
+ npx craft-brand --helper-import @my-lib/core
244
+
245
+ # Use specific tsconfig
246
+ npx craft-brand --tsconfig ./tsconfig.app.json
247
+ ```
248
+
249
+ #### Programmatic Usage
250
+
251
+ ```typescript
252
+ import { runAngularBrandCodemod } from '@craft-ng/dev-tools';
253
+
254
+ await runAngularBrandCodemod({
255
+ rootDir: './src',
256
+ dryRun: true,
257
+ transformOnlyStandaloneDeclarables: false,
258
+ includeProviders: true,
259
+ includeViewProviders: true,
260
+ helperImportPath: '@craft-ng/core',
261
+ log: console.log,
262
+ });
263
+ ```
264
+
265
+ #### What it does
266
+
267
+ Transforms this:
268
+
269
+ ```typescript
270
+ import { Component, inject } from '@angular/core';
271
+
272
+ @Component({
273
+ standalone: true,
274
+ imports: [CommonModule],
275
+ providers: [MyService],
276
+ })
277
+ export class DemoComponent {
278
+ private api = inject(ApiService);
279
+ constructor(private http: HttpClient) {}
280
+ }
281
+ ```
282
+
283
+ Into this:
284
+
285
+ ```typescript
286
+ import { Component, inject } from '@angular/core';
287
+ import { brandAngularSymbol, deps } from '@craft-ng/core';
288
+
289
+ @Component({
290
+ standalone: true,
291
+ imports: [CommonModule],
292
+ providers: [MyService],
293
+ })
294
+ class DemoComponent {
295
+ private api = inject(ApiService);
296
+ constructor(private http: HttpClient) {}
297
+ }
298
+
299
+ export default brandAngularSymbol(
300
+ DemoComponent,
301
+ deps({
302
+ injected: [HttpClient, ApiService],
303
+ importDeps: [CommonModule],
304
+ providers: [MyService],
305
+ }),
306
+ );
307
+ ```
308
+
309
+ #### CLI Options
310
+
311
+ | Option | Description | Default |
312
+ | ----------------------------------------- | -------------------------------------------------- | ---------------------- |
313
+ | `--root <dir>` | Project root directory | `cwd` |
314
+ | `--tsconfig <path>` | Path to tsconfig.json | `<root>/tsconfig.json` |
315
+ | `--helper-import <path>` | Import path for `brandAngularSymbol` and `deps` | `@craft-ng/core` |
316
+ | `--transform-only-standalone-declarables` | Only transform standalone components/directives | `false` |
317
+ | `--no-providers` | Do not include metadata providers in `deps()` | `false` |
318
+ | `--no-view-providers` | Do not include component viewProviders in `deps()` | `false` |
319
+ | `--dry-run` | Print results without writing files | `false` |
320
+ | `--help` | Show help | - |
321
+
322
+ ## License
323
+
324
+ MIT
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@craft-ng/dev-tools",
3
+ "version": "0.1.0",
4
+ "description": "Development tools for ng-craft: ESLint configs, ESLint rules, and codemods",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./eslint": "./src/eslint/index.mjs",
14
+ "./eslint/angular": "./src/eslint/angular.mjs",
15
+ "./eslint-rules": "./src/eslint-rules/index.cjs",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "bin": {
19
+ "craft-brand": "./src/bin/craft-brand.js"
20
+ },
21
+ "files": [
22
+ "src/**/*",
23
+ "README.md"
24
+ ],
25
+ "dependencies": {
26
+ "tslib": "^2.3.0",
27
+ "ts-morph": "^24.0.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@angular-eslint/eslint-plugin": ">=21.0.0",
31
+ "@typescript-eslint/eslint-plugin": ">=8.0.0",
32
+ "eslint": ">=9.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@angular-eslint/eslint-plugin": {
36
+ "optional": true
37
+ },
38
+ "@typescript-eslint/eslint-plugin": {
39
+ "optional": true
40
+ },
41
+ "eslint": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "keywords": [
46
+ "ng-craft",
47
+ "angular",
48
+ "codemod",
49
+ "eslint",
50
+ "eslint-config",
51
+ "eslint-plugin",
52
+ "typescript"
53
+ ],
54
+ "module": "./src/index.js"
55
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { runAngularBrandCodemod } from '../scripts/angular-brand-codemod.js';
3
+ runAngularBrandCodemod({
4
+ log: console.log,
5
+ }).catch((error) => {
6
+ console.error('Error running codemod:', error);
7
+ process.exit(1);
8
+ });
9
+ //# sourceMappingURL=craft-brand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"craft-brand.js","sourceRoot":"","sources":["../../../../../libs/dev-tools/src/bin/craft-brand.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAE7E,sBAAsB,CAAC;IACrB,GAAG,EAAE,OAAO,CAAC,GAAG;CACjB,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,26 @@
1
+ import baseConfig from './index.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.ts'],
7
+ rules: {
8
+ '@angular-eslint/directive-selector': [
9
+ 'error',
10
+ {
11
+ type: 'attribute',
12
+ prefix: 'app',
13
+ style: 'camelCase',
14
+ },
15
+ ],
16
+ '@angular-eslint/component-selector': [
17
+ 'error',
18
+ {
19
+ type: 'element',
20
+ prefix: 'app',
21
+ style: 'kebab-case',
22
+ },
23
+ ],
24
+ },
25
+ },
26
+ ];
@@ -0,0 +1,18 @@
1
+ export default [
2
+ {
3
+ files: ['**/*.ts'],
4
+ rules: {
5
+ '@typescript-eslint/no-unused-vars': [
6
+ 'warn',
7
+ {
8
+ argsIgnorePattern: '^_',
9
+ varsIgnorePattern: '^_',
10
+ caughtErrorsIgnorePattern: '^_',
11
+ },
12
+ ],
13
+ 'no-empty-object-type': 'off',
14
+ '@typescript-eslint/no-empty-object-type': 'off',
15
+ '@typescript-eslint/no-unsafe-function-type': 'off',
16
+ },
17
+ },
18
+ ];
@@ -0,0 +1,193 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { Project } = require('ts-morph');
5
+ const {
6
+ analyzeSourceFileDependencies,
7
+ readExistingDependencyGroups,
8
+ } = require('../scripts/angular-brand-codemod.js');
9
+
10
+ const projectCache = new Map();
11
+
12
+ module.exports = {
13
+ meta: {
14
+ type: 'problem',
15
+ docs: {
16
+ description:
17
+ 'Ensure deps({ injected, importDeps, providers }) matches the Angular symbol dependencies.',
18
+ },
19
+ schema: [],
20
+ },
21
+ create(context) {
22
+ return {
23
+ 'Program:exit'() {
24
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
25
+ const filePath = getFilePath(context);
26
+ if (!filePath || !filePath.endsWith('.ts')) {
27
+ return;
28
+ }
29
+
30
+ const text = sourceCode.getText();
31
+ if (!text.includes('brandAngularSymbol') || !text.includes('deps(')) {
32
+ return;
33
+ }
34
+
35
+ const sourceFile = getProjectSourceFile(
36
+ getProject(getCwd(context)),
37
+ filePath,
38
+ text,
39
+ );
40
+ const analysis = analyzeSourceFileDependencies(sourceFile);
41
+ if (
42
+ !analysis.classDeclaration ||
43
+ analysis.skipped ||
44
+ !analysis.className
45
+ ) {
46
+ return;
47
+ }
48
+
49
+ const existing = readExistingDependencyGroups(
50
+ sourceFile,
51
+ analysis.className,
52
+ );
53
+ if (!existing.found) {
54
+ return;
55
+ }
56
+
57
+ if (existing.warnings.length > 0) {
58
+ for (const warning of existing.warnings) {
59
+ reportNode(
60
+ context,
61
+ sourceCode,
62
+ existing.depsObjectNode ?? existing.exportAssignmentNode,
63
+ warning,
64
+ );
65
+ }
66
+ return;
67
+ }
68
+
69
+ reportIfMismatch(
70
+ context,
71
+ sourceCode,
72
+ existing,
73
+ 'injected',
74
+ analysis.dependencyGroups.injected,
75
+ existing.dependencyGroups.injected,
76
+ );
77
+ reportIfMismatch(
78
+ context,
79
+ sourceCode,
80
+ existing,
81
+ 'importDeps',
82
+ analysis.dependencyGroups.importDeps,
83
+ existing.dependencyGroups.importDeps,
84
+ );
85
+ reportIfMismatch(
86
+ context,
87
+ sourceCode,
88
+ existing,
89
+ 'providers',
90
+ analysis.dependencyGroups.providers,
91
+ existing.dependencyGroups.providers,
92
+ );
93
+ },
94
+ };
95
+ },
96
+ };
97
+
98
+ function getProject(cwd) {
99
+ let project = projectCache.get(cwd);
100
+ if (project) {
101
+ return project;
102
+ }
103
+
104
+ const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
105
+ project = fs.existsSync(tsConfigFilePath)
106
+ ? new Project({ tsConfigFilePath })
107
+ : new Project({
108
+ compilerOptions: {
109
+ experimentalDecorators: true,
110
+ target: 9,
111
+ },
112
+ });
113
+
114
+ projectCache.set(cwd, project);
115
+ return project;
116
+ }
117
+
118
+ function getProjectSourceFile(project, filePath, text) {
119
+ const normalizedPath = path.resolve(filePath);
120
+ const existingSourceFile = project.getSourceFile(normalizedPath);
121
+ if (existingSourceFile) {
122
+ existingSourceFile.replaceWithText(text);
123
+ return existingSourceFile;
124
+ }
125
+
126
+ const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
127
+ if (sourceFile) {
128
+ sourceFile.replaceWithText(text);
129
+ return sourceFile;
130
+ }
131
+
132
+ return project.createSourceFile(normalizedPath, text, { overwrite: true });
133
+ }
134
+
135
+ function getFilePath(context) {
136
+ const filePath = context.filename ?? context.getFilename();
137
+ if (!filePath || filePath === '<input>') {
138
+ return undefined;
139
+ }
140
+
141
+ return filePath;
142
+ }
143
+
144
+ function getCwd(context) {
145
+ return context.cwd ?? process.cwd();
146
+ }
147
+
148
+ function reportIfMismatch(
149
+ context,
150
+ sourceCode,
151
+ existing,
152
+ propertyName,
153
+ expected,
154
+ actual,
155
+ ) {
156
+ if (isSameDependencyList(expected, actual)) {
157
+ return;
158
+ }
159
+
160
+ reportNode(
161
+ context,
162
+ sourceCode,
163
+ existing.propertyNodes[propertyName] ??
164
+ existing.depsObjectNode ??
165
+ existing.exportAssignmentNode,
166
+ `deps.${propertyName} does not match the Angular symbol. Expected ${formatDependencyList(expected)} but found ${formatDependencyList(actual)}.`,
167
+ );
168
+ }
169
+
170
+ function reportNode(context, sourceCode, node, message) {
171
+ context.report({
172
+ loc: node ? getNodeLoc(sourceCode, node) : undefined,
173
+ message,
174
+ });
175
+ }
176
+
177
+ function getNodeLoc(sourceCode, node) {
178
+ return {
179
+ start: sourceCode.getLocFromIndex(node.getStart()),
180
+ end: sourceCode.getLocFromIndex(node.getEnd()),
181
+ };
182
+ }
183
+
184
+ function isSameDependencyList(left, right) {
185
+ return (
186
+ left.length === right.length &&
187
+ left.every((value, index) => value === right[index])
188
+ );
189
+ }
190
+
191
+ function formatDependencyList(dependencies) {
192
+ return `[${dependencies.join(', ')}]`;
193
+ }
@@ -0,0 +1,12 @@
1
+ // ESLint rules for ng-craft
2
+ const brandAngularDepsMatch = require('./brand-angular-deps-match.cjs');
3
+ const noAngularInject = require('./no-angular-inject.cjs');
4
+ const noDirectAngularClassExport = require('./no-direct-angular-class-export.cjs');
5
+
6
+ module.exports = {
7
+ rules: {
8
+ 'brand-angular-deps-match': brandAngularDepsMatch,
9
+ 'no-angular-inject': noAngularInject,
10
+ 'no-direct-angular-class-export': noDirectAngularClassExport,
11
+ },
12
+ };