angular-grab 0.1.1 → 0.1.2
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 +215 -0
- package/examples/angular-19-app/.editorconfig +17 -0
- package/examples/angular-19-app/.vscode/extensions.json +4 -0
- package/examples/angular-19-app/.vscode/launch.json +20 -0
- package/examples/angular-19-app/.vscode/mcp.json +9 -0
- package/examples/angular-19-app/.vscode/tasks.json +42 -0
- package/examples/angular-19-app/README.md +59 -0
- package/examples/angular-19-app/angular.json +74 -0
- package/examples/angular-19-app/package.json +44 -0
- package/examples/angular-19-app/public/favicon.ico +0 -0
- package/examples/angular-19-app/src/app/app.config.ts +13 -0
- package/examples/angular-19-app/src/app/app.css +37 -0
- package/examples/angular-19-app/src/app/app.html +25 -0
- package/examples/angular-19-app/src/app/app.routes.ts +3 -0
- package/examples/angular-19-app/src/app/app.spec.ts +23 -0
- package/examples/angular-19-app/src/app/app.ts +12 -0
- package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
- package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
- package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
- package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
- package/examples/angular-19-app/src/index.html +13 -0
- package/examples/angular-19-app/src/main.ts +6 -0
- package/examples/angular-19-app/src/styles.css +1 -0
- package/examples/angular-19-app/tsconfig.app.json +15 -0
- package/examples/angular-19-app/tsconfig.json +33 -0
- package/examples/angular-19-app/tsconfig.spec.json +15 -0
- package/package.json +14 -111
- package/packages/angular-grab/package.json +96 -0
- package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
- package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
- package/packages/angular-grab/src/angular/index.ts +13 -0
- package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
- package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
- package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
- package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
- package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
- package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
- package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
- package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
- package/packages/angular-grab/src/builder/index.ts +3 -0
- package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
- package/packages/angular-grab/src/cli/commands/init.ts +106 -0
- package/packages/angular-grab/src/cli/index.ts +15 -0
- package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
- package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
- package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
- package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
- package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
- package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
- package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
- package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
- package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
- package/packages/angular-grab/src/core/constants.ts +10 -0
- package/packages/angular-grab/src/core/grab.ts +596 -0
- package/packages/angular-grab/src/core/index.global.ts +13 -0
- package/packages/angular-grab/src/core/index.ts +19 -0
- package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
- package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
- package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
- package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
- package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
- package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
- package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
- package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
- package/packages/angular-grab/src/core/store.ts +52 -0
- package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
- package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
- package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
- package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
- package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
- package/packages/angular-grab/src/core/types.ts +139 -0
- package/packages/angular-grab/src/core/utils.ts +16 -0
- package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
- package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
- package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
- package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
- package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
- package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
- package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
- package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
- package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
- package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
- package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
- package/packages/angular-grab/tsconfig.json +15 -0
- package/packages/angular-grab/tsup.config.ts +119 -0
- package/pnpm-workspace.yaml +3 -0
- package/turbo.json +21 -0
- package/dist/angular/index.d.ts +0 -151
- package/dist/angular/index.js +0 -2811
- package/dist/angular/index.js.map +0 -1
- package/dist/builder/builders/application/index.js +0 -143
- package/dist/builder/builders/application/index.js.map +0 -1
- package/dist/builder/builders/dev-server/index.js +0 -139
- package/dist/builder/builders/dev-server/index.js.map +0 -1
- package/dist/builder/index.js +0 -2
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/package.json +0 -1
- package/dist/cli/index.js +0 -223
- package/dist/cli/index.js.map +0 -1
- package/dist/core/index.cjs +0 -2589
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -139
- package/dist/core/index.d.ts +0 -139
- package/dist/core/index.global.js +0 -542
- package/dist/core/index.js +0 -2560
- package/dist/core/index.js.map +0 -1
- package/dist/esbuild-plugin/index.cjs +0 -239
- package/dist/esbuild-plugin/index.cjs.map +0 -1
- package/dist/esbuild-plugin/index.d.cts +0 -26
- package/dist/esbuild-plugin/index.d.ts +0 -26
- package/dist/esbuild-plugin/index.js +0 -200
- package/dist/esbuild-plugin/index.js.map +0 -1
- package/dist/vite-plugin/index.d.ts +0 -7
- package/dist/vite-plugin/index.js +0 -128
- package/dist/vite-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/index.cjs +0 -54
- package/dist/webpack-plugin/index.cjs.map +0 -1
- package/dist/webpack-plugin/index.d.cts +0 -5
- package/dist/webpack-plugin/index.d.ts +0 -5
- package/dist/webpack-plugin/index.js +0 -23
- package/dist/webpack-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/loader.cjs +0 -155
- package/dist/webpack-plugin/loader.cjs.map +0 -1
- package/dist/webpack-plugin/loader.d.cts +0 -3
- package/dist/webpack-plugin/loader.d.ts +0 -3
- package/dist/webpack-plugin/loader.js +0 -122
- package/dist/webpack-plugin/loader.js.map +0 -1
- /package/{builders.json → packages/angular-grab/builders.json} +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/dev-server/schema.json +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { findAngularJson, detectProject } from '../utils/detect-project';
|
|
6
|
+
|
|
7
|
+
describe('findAngularJson', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = mkdtempSync(join(tmpdir(), 'cli-test-'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('finds angular.json in the given directory', () => {
|
|
19
|
+
writeFileSync(join(tempDir, 'angular.json'), '{}');
|
|
20
|
+
const result = findAngularJson(tempDir);
|
|
21
|
+
expect(result).toBe(join(tempDir, 'angular.json'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('finds angular.json in a parent directory', () => {
|
|
25
|
+
writeFileSync(join(tempDir, 'angular.json'), '{}');
|
|
26
|
+
const subDir = join(tempDir, 'src', 'app');
|
|
27
|
+
mkdirSync(subDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
const result = findAngularJson(subDir);
|
|
30
|
+
expect(result).toBe(join(tempDir, 'angular.json'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null when no angular.json exists', () => {
|
|
34
|
+
const result = findAngularJson(tempDir);
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('detectProject', () => {
|
|
40
|
+
let tempDir: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tempDir = mkdtempSync(join(tmpdir(), 'cli-test-'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function writeAngularJson(content: object): string {
|
|
51
|
+
const path = join(tempDir, 'angular.json');
|
|
52
|
+
writeFileSync(path, JSON.stringify(content));
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('detects project name from angular.json', () => {
|
|
57
|
+
const path = writeAngularJson({
|
|
58
|
+
projects: {
|
|
59
|
+
'my-app': {
|
|
60
|
+
sourceRoot: 'src',
|
|
61
|
+
architect: {
|
|
62
|
+
build: { builder: '@angular/build:application' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const info = detectProject(path);
|
|
69
|
+
expect(info.projectName).toBe('my-app');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('detects "application" builder type', () => {
|
|
73
|
+
const path = writeAngularJson({
|
|
74
|
+
projects: {
|
|
75
|
+
app: {
|
|
76
|
+
architect: {
|
|
77
|
+
build: { builder: '@angular/build:application' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const info = detectProject(path);
|
|
84
|
+
expect(info.builderType).toBe('application');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('detects "browser-esbuild" builder type', () => {
|
|
88
|
+
const path = writeAngularJson({
|
|
89
|
+
projects: {
|
|
90
|
+
app: {
|
|
91
|
+
architect: {
|
|
92
|
+
build: { builder: '@angular-devkit/build-angular:browser-esbuild' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const info = detectProject(path);
|
|
99
|
+
expect(info.builderType).toBe('browser-esbuild');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('detects "browser" builder type', () => {
|
|
103
|
+
const path = writeAngularJson({
|
|
104
|
+
projects: {
|
|
105
|
+
app: {
|
|
106
|
+
architect: {
|
|
107
|
+
build: { builder: '@angular-devkit/build-angular:browser' },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const info = detectProject(path);
|
|
114
|
+
expect(info.builderType).toBe('browser');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('detects angular-grab:application as "application"', () => {
|
|
118
|
+
const path = writeAngularJson({
|
|
119
|
+
projects: {
|
|
120
|
+
app: {
|
|
121
|
+
architect: {
|
|
122
|
+
build: { builder: 'angular-grab:application' },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const info = detectProject(path);
|
|
129
|
+
expect(info.builderType).toBe('application');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('defaults sourceRoot to "src" when not specified', () => {
|
|
133
|
+
const path = writeAngularJson({
|
|
134
|
+
projects: {
|
|
135
|
+
app: {
|
|
136
|
+
architect: {
|
|
137
|
+
build: { builder: '@angular/build:application' },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const info = detectProject(path);
|
|
144
|
+
expect(info.sourceRoot).toBe('src');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('uses custom sourceRoot when specified', () => {
|
|
148
|
+
const path = writeAngularJson({
|
|
149
|
+
projects: {
|
|
150
|
+
app: {
|
|
151
|
+
sourceRoot: 'projects/app/src',
|
|
152
|
+
architect: {
|
|
153
|
+
build: { builder: '@angular/build:application' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const info = detectProject(path);
|
|
160
|
+
expect(info.sourceRoot).toBe('projects/app/src');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('throws when no projects are found', () => {
|
|
164
|
+
const path = writeAngularJson({ projects: {} });
|
|
165
|
+
expect(() => detectProject(path)).toThrow('No projects found');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('throws when no build target is found', () => {
|
|
169
|
+
const path = writeAngularJson({
|
|
170
|
+
projects: {
|
|
171
|
+
app: { architect: {} },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(() => detectProject(path)).toThrow('No build target found');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('detects npm as default package manager', () => {
|
|
179
|
+
const path = writeAngularJson({
|
|
180
|
+
projects: {
|
|
181
|
+
app: {
|
|
182
|
+
architect: {
|
|
183
|
+
build: { builder: '@angular/build:application' },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const info = detectProject(path);
|
|
190
|
+
expect(info.packageManager).toBe('npm');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('detects pnpm when pnpm-lock.yaml exists', () => {
|
|
194
|
+
writeFileSync(join(tempDir, 'pnpm-lock.yaml'), '');
|
|
195
|
+
const path = writeAngularJson({
|
|
196
|
+
projects: {
|
|
197
|
+
app: {
|
|
198
|
+
architect: {
|
|
199
|
+
build: { builder: '@angular/build:application' },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const info = detectProject(path);
|
|
206
|
+
expect(info.packageManager).toBe('pnpm');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('detects yarn when yarn.lock exists', () => {
|
|
210
|
+
writeFileSync(join(tempDir, 'yarn.lock'), '');
|
|
211
|
+
const path = writeAngularJson({
|
|
212
|
+
projects: {
|
|
213
|
+
app: {
|
|
214
|
+
architect: {
|
|
215
|
+
build: { builder: '@angular/build:application' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const info = detectProject(path);
|
|
222
|
+
expect(info.packageManager).toBe('yarn');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('supports "targets" as an alternative to "architect"', () => {
|
|
226
|
+
const path = writeAngularJson({
|
|
227
|
+
projects: {
|
|
228
|
+
app: {
|
|
229
|
+
targets: {
|
|
230
|
+
build: { builder: '@angular/build:application' },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const info = detectProject(path);
|
|
237
|
+
expect(info.builderType).toBe('application');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { findAngularJson, detectProject, type ProjectInfo } from '../utils/detect-project';
|
|
3
|
+
import { modifyAngularJson } from '../utils/modify-angular-json';
|
|
4
|
+
import { modifyAppConfig } from '../utils/modify-app-config';
|
|
5
|
+
|
|
6
|
+
const PACKAGES = ['angular-grab'];
|
|
7
|
+
|
|
8
|
+
function log(msg: string): void {
|
|
9
|
+
console.log(`\x1b[36m[angular-grab]\x1b[0m ${msg}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function warn(msg: string): void {
|
|
13
|
+
console.log(`\x1b[33m[angular-grab]\x1b[0m ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getInstallCommand(pm: ProjectInfo['packageManager']): string {
|
|
17
|
+
const deps = PACKAGES.join(' ');
|
|
18
|
+
switch (pm) {
|
|
19
|
+
case 'pnpm':
|
|
20
|
+
return `pnpm add -D ${deps}`;
|
|
21
|
+
case 'yarn':
|
|
22
|
+
return `yarn add -D ${deps}`;
|
|
23
|
+
default:
|
|
24
|
+
return `npm install -D ${deps}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function init(): Promise<void> {
|
|
29
|
+
log('Initializing angular-grab...\n');
|
|
30
|
+
|
|
31
|
+
// 1. Find angular.json
|
|
32
|
+
const angularJsonPath = findAngularJson();
|
|
33
|
+
if (!angularJsonPath) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'Could not find angular.json. Are you in an Angular project directory?',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Detect project info
|
|
40
|
+
const info = detectProject(angularJsonPath);
|
|
41
|
+
log(`Found project: ${info.projectName}`);
|
|
42
|
+
log(`Builder type: ${info.builderType}`);
|
|
43
|
+
log(`Package manager: ${info.packageManager}`);
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// 3. Reject legacy builders
|
|
47
|
+
if (info.builderType === 'browser') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'angular-grab requires the "application" builder (Angular 17+).\n' +
|
|
50
|
+
'Please migrate from "@angular-devkit/build-angular:browser" to "@angular/build:application".\n' +
|
|
51
|
+
'See: https://angular.dev/tools/cli/build-system-migration',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (info.builderType === 'browser-esbuild') {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'angular-grab requires the "application" builder, not the transitional "browser-esbuild" builder.\n' +
|
|
58
|
+
'Please migrate from "@angular-devkit/build-angular:browser-esbuild" to "@angular/build:application".\n' +
|
|
59
|
+
'See: https://angular.dev/tools/cli/build-system-migration',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Install packages
|
|
64
|
+
log('Installing packages...');
|
|
65
|
+
const installCmd = getInstallCommand(info.packageManager);
|
|
66
|
+
try {
|
|
67
|
+
execSync(installCmd, {
|
|
68
|
+
cwd: info.projectRoot,
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
warn('Package installation failed. You may need to install manually:');
|
|
73
|
+
warn(` ${installCmd}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 5. Modify angular.json
|
|
78
|
+
log('Updating angular.json...');
|
|
79
|
+
const jsonModified = modifyAngularJson(info.angularJsonPath, info.projectName);
|
|
80
|
+
if (jsonModified) {
|
|
81
|
+
log(' Swapped builders to angular-grab');
|
|
82
|
+
} else {
|
|
83
|
+
warn(' angular.json builders were already configured or could not be modified');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Try to add provideAngularGrab() to app config
|
|
87
|
+
log('Updating app.config.ts...');
|
|
88
|
+
const configModified = modifyAppConfig(info.projectRoot, info.sourceRoot);
|
|
89
|
+
if (configModified) {
|
|
90
|
+
log(' Added provideAngularGrab() to providers');
|
|
91
|
+
} else {
|
|
92
|
+
warn(' Could not modify app.config.ts (already configured or not found)');
|
|
93
|
+
warn(' Please add manually:');
|
|
94
|
+
warn(" import { provideAngularGrab } from 'angular-grab/angular';");
|
|
95
|
+
warn(' // then add provideAngularGrab() to your providers array');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('');
|
|
99
|
+
log('\x1b[32mDone!\x1b[0m angular-grab is ready.');
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(' Next steps:');
|
|
102
|
+
console.log(' 1. Run \x1b[1mng serve\x1b[0m');
|
|
103
|
+
console.log(' 2. Hold \x1b[1mCmd+C\x1b[0m (Mac) or \x1b[1mCtrl+C\x1b[0m (Windows) and hover over elements');
|
|
104
|
+
console.log(' 3. Click to copy element context to clipboard');
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { init } from './commands/init';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args[0];
|
|
5
|
+
|
|
6
|
+
if (command === 'init' || !command) {
|
|
7
|
+
init().catch((err) => {
|
|
8
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
} else {
|
|
12
|
+
console.error(`Unknown command: ${command}`);
|
|
13
|
+
console.log('\nUsage: npx angular-grab init');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ProjectInfo {
|
|
5
|
+
angularJsonPath: string;
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
projectName: string;
|
|
8
|
+
sourceRoot: string;
|
|
9
|
+
builderType: 'application' | 'browser-esbuild' | 'browser';
|
|
10
|
+
packageManager: 'pnpm' | 'yarn' | 'npm';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findAngularJson(startDir: string = process.cwd()): string | null {
|
|
14
|
+
let dir = startDir;
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = join(dir, 'angular.json');
|
|
17
|
+
if (existsSync(candidate)) return candidate;
|
|
18
|
+
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) return null;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectProject(angularJsonPath: string): ProjectInfo {
|
|
26
|
+
const projectRoot = dirname(angularJsonPath);
|
|
27
|
+
const raw = readFileSync(angularJsonPath, 'utf8');
|
|
28
|
+
const angularJson = JSON.parse(raw);
|
|
29
|
+
|
|
30
|
+
const projects = angularJson.projects || {};
|
|
31
|
+
const projectNames = Object.keys(projects);
|
|
32
|
+
if (projectNames.length === 0) {
|
|
33
|
+
throw new Error('No projects found in angular.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const projectName = projectNames[0];
|
|
37
|
+
const project = projects[projectName];
|
|
38
|
+
if (!project) {
|
|
39
|
+
throw new Error(`Project "${projectName}" not found in angular.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sourceRoot = project.sourceRoot || 'src';
|
|
43
|
+
const targets = project.architect || project.targets;
|
|
44
|
+
const buildTarget = targets?.build;
|
|
45
|
+
if (!buildTarget) {
|
|
46
|
+
throw new Error(`No build target found for project "${projectName}"`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const builder: string = buildTarget.builder || '';
|
|
50
|
+
let builderType: ProjectInfo['builderType'];
|
|
51
|
+
|
|
52
|
+
if (builder.includes(':application') || builder.includes('angular-grab:application')) {
|
|
53
|
+
builderType = 'application';
|
|
54
|
+
} else if (builder.includes(':browser-esbuild')) {
|
|
55
|
+
builderType = 'browser-esbuild';
|
|
56
|
+
} else if (builder.includes(':browser')) {
|
|
57
|
+
builderType = 'browser';
|
|
58
|
+
} else {
|
|
59
|
+
builderType = 'application';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const packageManager = detectPackageManager(projectRoot);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
angularJsonPath,
|
|
66
|
+
projectRoot,
|
|
67
|
+
projectName,
|
|
68
|
+
sourceRoot,
|
|
69
|
+
builderType,
|
|
70
|
+
packageManager,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectPackageManager(root: string): 'pnpm' | 'yarn' | 'npm' {
|
|
75
|
+
if (existsSync(join(root, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
76
|
+
if (existsSync(join(root, 'yarn.lock'))) return 'yarn';
|
|
77
|
+
return 'npm';
|
|
78
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export function modifyAngularJson(angularJsonPath: string, projectName: string): boolean {
|
|
4
|
+
const raw = readFileSync(angularJsonPath, 'utf8');
|
|
5
|
+
const angularJson = JSON.parse(raw);
|
|
6
|
+
|
|
7
|
+
const project = angularJson.projects?.[projectName];
|
|
8
|
+
const targets = project?.architect || project?.targets;
|
|
9
|
+
if (!targets) return false;
|
|
10
|
+
|
|
11
|
+
let modified = false;
|
|
12
|
+
|
|
13
|
+
const buildTarget = targets.build;
|
|
14
|
+
if (buildTarget?.builder) {
|
|
15
|
+
const currentBuilder: string = buildTarget.builder;
|
|
16
|
+
if (
|
|
17
|
+
currentBuilder === '@angular/build:application' ||
|
|
18
|
+
currentBuilder === '@angular-devkit/build-angular:application'
|
|
19
|
+
) {
|
|
20
|
+
buildTarget.builder = 'angular-grab:application';
|
|
21
|
+
modified = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const serveTarget = targets.serve;
|
|
26
|
+
if (serveTarget?.builder) {
|
|
27
|
+
const currentBuilder: string = serveTarget.builder;
|
|
28
|
+
if (
|
|
29
|
+
currentBuilder === '@angular/build:dev-server' ||
|
|
30
|
+
currentBuilder === '@angular-devkit/build-angular:dev-server'
|
|
31
|
+
) {
|
|
32
|
+
serveTarget.builder = 'angular-grab:dev-server';
|
|
33
|
+
modified = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (modified) {
|
|
38
|
+
writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n', 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return modified;
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export function modifyAppConfig(projectRoot: string, sourceRoot: string): boolean {
|
|
5
|
+
const configPath = join(projectRoot, sourceRoot, 'app', 'app.config.ts');
|
|
6
|
+
if (!existsSync(configPath)) return false;
|
|
7
|
+
|
|
8
|
+
let content = readFileSync(configPath, 'utf8');
|
|
9
|
+
|
|
10
|
+
// Skip if already has provideAngularGrab
|
|
11
|
+
if (content.includes('provideAngularGrab')) return false;
|
|
12
|
+
|
|
13
|
+
// Add import for provideAngularGrab
|
|
14
|
+
const importStatement = "import { provideAngularGrab } from 'angular-grab/angular';";
|
|
15
|
+
|
|
16
|
+
// Find the last import block end by matching `from '...'` or `from "..."` lines
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
let lastImportEndIndex = -1;
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const trimmed = lines[i].trimStart();
|
|
21
|
+
if (trimmed.startsWith('import ') || /\}\s*from\s+['"]/.test(trimmed) || /from\s+['"]/.test(trimmed)) {
|
|
22
|
+
lastImportEndIndex = i;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lastImportEndIndex >= 0) {
|
|
27
|
+
lines.splice(lastImportEndIndex + 1, 0, importStatement);
|
|
28
|
+
content = lines.join('\n');
|
|
29
|
+
} else {
|
|
30
|
+
content = importStatement + '\n' + content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Try to add provideAngularGrab() to the providers array
|
|
34
|
+
const providersMatch = content.match(/providers\s*:\s*\[/);
|
|
35
|
+
if (providersMatch && providersMatch.index != null) {
|
|
36
|
+
const insertPos = providersMatch.index + providersMatch[0].length;
|
|
37
|
+
content = content.slice(0, insertPos) + '\n provideAngularGrab(),' + content.slice(insertPos);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writeFileSync(configPath, content, 'utf8');
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { generateSnippet } from '../clipboard/generate-snippet';
|
|
4
|
+
import type { ElementContext, ComponentStackEntry } from '../types';
|
|
5
|
+
|
|
6
|
+
function makeContext(overrides: Partial<ElementContext> = {}): ElementContext {
|
|
7
|
+
return {
|
|
8
|
+
element: document.createElement('div'),
|
|
9
|
+
html: '<div>hello</div>',
|
|
10
|
+
componentName: null,
|
|
11
|
+
filePath: null,
|
|
12
|
+
line: null,
|
|
13
|
+
column: null,
|
|
14
|
+
componentStack: [],
|
|
15
|
+
selector: 'div',
|
|
16
|
+
cssClasses: [],
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('generateSnippet', () => {
|
|
22
|
+
it('generates HTML-only snippet when no component info', () => {
|
|
23
|
+
const ctx = makeContext({ html: '<button>Click</button>' });
|
|
24
|
+
const result = generateSnippet(ctx, 20);
|
|
25
|
+
expect(result).toBe('<button>Click</button>');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('includes component name and file path', () => {
|
|
29
|
+
const ctx = makeContext({
|
|
30
|
+
html: '<div>test</div>',
|
|
31
|
+
componentName: 'AppComponent',
|
|
32
|
+
filePath: 'src/app/app.component.ts',
|
|
33
|
+
line: 10,
|
|
34
|
+
column: 5,
|
|
35
|
+
});
|
|
36
|
+
const result = generateSnippet(ctx, 20);
|
|
37
|
+
|
|
38
|
+
expect(result).toContain('<div>test</div>');
|
|
39
|
+
expect(result).toContain('in AppComponent');
|
|
40
|
+
expect(result).toContain('at src/app/app.component.ts:10:5');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('includes component name without file path', () => {
|
|
44
|
+
const ctx = makeContext({ componentName: 'MyComponent' });
|
|
45
|
+
const result = generateSnippet(ctx, 20);
|
|
46
|
+
|
|
47
|
+
expect(result).toContain('in MyComponent');
|
|
48
|
+
expect(result).not.toContain('at');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('includes file path without component name', () => {
|
|
52
|
+
const ctx = makeContext({
|
|
53
|
+
filePath: 'src/app.ts',
|
|
54
|
+
line: 5,
|
|
55
|
+
});
|
|
56
|
+
const result = generateSnippet(ctx, 20);
|
|
57
|
+
|
|
58
|
+
expect(result).toContain('at src/app.ts:5');
|
|
59
|
+
expect(result).not.toContain('in ');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('includes component stack trace', () => {
|
|
63
|
+
const stack: ComponentStackEntry[] = [
|
|
64
|
+
{ name: 'ChildComponent', filePath: 'src/child.ts', line: 3, column: 1 },
|
|
65
|
+
{ name: 'ParentComponent', filePath: 'src/parent.ts', line: 10, column: null },
|
|
66
|
+
{ name: 'AppComponent', filePath: 'src/app.ts', line: null, column: null },
|
|
67
|
+
];
|
|
68
|
+
const ctx = makeContext({
|
|
69
|
+
html: '<span>hi</span>',
|
|
70
|
+
componentStack: stack,
|
|
71
|
+
});
|
|
72
|
+
const result = generateSnippet(ctx, 20);
|
|
73
|
+
|
|
74
|
+
expect(result).toContain('in ChildComponent at src/child.ts:3:1');
|
|
75
|
+
expect(result).toContain('in ParentComponent at src/parent.ts:10');
|
|
76
|
+
expect(result).toContain('in AppComponent at src/app.ts');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('prefers component stack over componentName/filePath', () => {
|
|
80
|
+
const stack: ComponentStackEntry[] = [
|
|
81
|
+
{ name: 'Inner', filePath: 'inner.ts', line: 1, column: null },
|
|
82
|
+
];
|
|
83
|
+
const ctx = makeContext({
|
|
84
|
+
componentName: 'Outer',
|
|
85
|
+
filePath: 'outer.ts',
|
|
86
|
+
line: 99,
|
|
87
|
+
column: null,
|
|
88
|
+
componentStack: stack,
|
|
89
|
+
});
|
|
90
|
+
const result = generateSnippet(ctx, 20);
|
|
91
|
+
|
|
92
|
+
expect(result).toContain('in Inner at inner.ts:1');
|
|
93
|
+
expect(result).not.toContain('Outer');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('respects maxLines truncation', () => {
|
|
97
|
+
const multilineHtml = Array.from({ length: 10 }, (_, i) => ` <line${i}/>`).join('\n');
|
|
98
|
+
const ctx = makeContext({ html: multilineHtml });
|
|
99
|
+
const result = generateSnippet(ctx, 3);
|
|
100
|
+
|
|
101
|
+
const lines = result.split('\n');
|
|
102
|
+
// 3 HTML lines + " ..." truncation line
|
|
103
|
+
expect(lines.length).toBe(4);
|
|
104
|
+
expect(lines[3]).toBe(' ...');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not truncate when HTML is within maxLines', () => {
|
|
108
|
+
const html = '<div>\n <span>hi</span>\n</div>';
|
|
109
|
+
const ctx = makeContext({ html });
|
|
110
|
+
const result = generateSnippet(ctx, 10);
|
|
111
|
+
|
|
112
|
+
expect(result).not.toContain('...');
|
|
113
|
+
expect(result).toBe(html);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('cleans Angular attributes from HTML', () => {
|
|
117
|
+
const html = '<div _nghost-abc-123="">content</div>';
|
|
118
|
+
const ctx = makeContext({ html });
|
|
119
|
+
const result = generateSnippet(ctx, 20);
|
|
120
|
+
|
|
121
|
+
expect(result).toBe('<div>content</div>');
|
|
122
|
+
expect(result).not.toContain('_nghost');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('handles null fields gracefully', () => {
|
|
126
|
+
const ctx = makeContext({
|
|
127
|
+
componentName: null,
|
|
128
|
+
filePath: null,
|
|
129
|
+
line: null,
|
|
130
|
+
column: null,
|
|
131
|
+
componentStack: [],
|
|
132
|
+
});
|
|
133
|
+
const result = generateSnippet(ctx, 20);
|
|
134
|
+
|
|
135
|
+
// Should just be the HTML, no location info
|
|
136
|
+
expect(result).toBe('<div>hello</div>');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles stack entry with null filePath', () => {
|
|
140
|
+
const stack: ComponentStackEntry[] = [
|
|
141
|
+
{ name: 'Comp', filePath: null, line: null, column: null },
|
|
142
|
+
];
|
|
143
|
+
const ctx = makeContext({ componentStack: stack });
|
|
144
|
+
const result = generateSnippet(ctx, 20);
|
|
145
|
+
|
|
146
|
+
expect(result).toContain('in Comp');
|
|
147
|
+
expect(result).not.toContain('at');
|
|
148
|
+
});
|
|
149
|
+
});
|