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.
Files changed (132) hide show
  1. package/README.md +215 -0
  2. package/examples/angular-19-app/.editorconfig +17 -0
  3. package/examples/angular-19-app/.vscode/extensions.json +4 -0
  4. package/examples/angular-19-app/.vscode/launch.json +20 -0
  5. package/examples/angular-19-app/.vscode/mcp.json +9 -0
  6. package/examples/angular-19-app/.vscode/tasks.json +42 -0
  7. package/examples/angular-19-app/README.md +59 -0
  8. package/examples/angular-19-app/angular.json +74 -0
  9. package/examples/angular-19-app/package.json +44 -0
  10. package/examples/angular-19-app/public/favicon.ico +0 -0
  11. package/examples/angular-19-app/src/app/app.config.ts +13 -0
  12. package/examples/angular-19-app/src/app/app.css +37 -0
  13. package/examples/angular-19-app/src/app/app.html +25 -0
  14. package/examples/angular-19-app/src/app/app.routes.ts +3 -0
  15. package/examples/angular-19-app/src/app/app.spec.ts +23 -0
  16. package/examples/angular-19-app/src/app/app.ts +12 -0
  17. package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
  18. package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
  19. package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
  20. package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
  21. package/examples/angular-19-app/src/index.html +13 -0
  22. package/examples/angular-19-app/src/main.ts +6 -0
  23. package/examples/angular-19-app/src/styles.css +1 -0
  24. package/examples/angular-19-app/tsconfig.app.json +15 -0
  25. package/examples/angular-19-app/tsconfig.json +33 -0
  26. package/examples/angular-19-app/tsconfig.spec.json +15 -0
  27. package/package.json +14 -111
  28. package/packages/angular-grab/package.json +96 -0
  29. package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
  30. package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
  31. package/packages/angular-grab/src/angular/index.ts +13 -0
  32. package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
  33. package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
  34. package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
  35. package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
  36. package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
  37. package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
  38. package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
  39. package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
  40. package/packages/angular-grab/src/builder/index.ts +3 -0
  41. package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
  42. package/packages/angular-grab/src/cli/commands/init.ts +106 -0
  43. package/packages/angular-grab/src/cli/index.ts +15 -0
  44. package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
  45. package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
  46. package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
  47. package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
  48. package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
  49. package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
  50. package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
  51. package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
  52. package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
  53. package/packages/angular-grab/src/core/constants.ts +10 -0
  54. package/packages/angular-grab/src/core/grab.ts +596 -0
  55. package/packages/angular-grab/src/core/index.global.ts +13 -0
  56. package/packages/angular-grab/src/core/index.ts +19 -0
  57. package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
  58. package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
  59. package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
  60. package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
  61. package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
  62. package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
  63. package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
  64. package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
  65. package/packages/angular-grab/src/core/store.ts +52 -0
  66. package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
  67. package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
  68. package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
  69. package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
  70. package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
  71. package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
  72. package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
  73. package/packages/angular-grab/src/core/types.ts +139 -0
  74. package/packages/angular-grab/src/core/utils.ts +16 -0
  75. package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
  76. package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
  77. package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
  78. package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
  79. package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
  80. package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
  81. package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
  82. package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
  83. package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
  84. package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
  85. package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
  86. package/packages/angular-grab/tsconfig.json +15 -0
  87. package/packages/angular-grab/tsup.config.ts +119 -0
  88. package/pnpm-workspace.yaml +3 -0
  89. package/turbo.json +21 -0
  90. package/dist/angular/index.d.ts +0 -151
  91. package/dist/angular/index.js +0 -2811
  92. package/dist/angular/index.js.map +0 -1
  93. package/dist/builder/builders/application/index.js +0 -143
  94. package/dist/builder/builders/application/index.js.map +0 -1
  95. package/dist/builder/builders/dev-server/index.js +0 -139
  96. package/dist/builder/builders/dev-server/index.js.map +0 -1
  97. package/dist/builder/index.js +0 -2
  98. package/dist/builder/index.js.map +0 -1
  99. package/dist/builder/package.json +0 -1
  100. package/dist/cli/index.js +0 -223
  101. package/dist/cli/index.js.map +0 -1
  102. package/dist/core/index.cjs +0 -2589
  103. package/dist/core/index.cjs.map +0 -1
  104. package/dist/core/index.d.cts +0 -139
  105. package/dist/core/index.d.ts +0 -139
  106. package/dist/core/index.global.js +0 -542
  107. package/dist/core/index.js +0 -2560
  108. package/dist/core/index.js.map +0 -1
  109. package/dist/esbuild-plugin/index.cjs +0 -239
  110. package/dist/esbuild-plugin/index.cjs.map +0 -1
  111. package/dist/esbuild-plugin/index.d.cts +0 -26
  112. package/dist/esbuild-plugin/index.d.ts +0 -26
  113. package/dist/esbuild-plugin/index.js +0 -200
  114. package/dist/esbuild-plugin/index.js.map +0 -1
  115. package/dist/vite-plugin/index.d.ts +0 -7
  116. package/dist/vite-plugin/index.js +0 -128
  117. package/dist/vite-plugin/index.js.map +0 -1
  118. package/dist/webpack-plugin/index.cjs +0 -54
  119. package/dist/webpack-plugin/index.cjs.map +0 -1
  120. package/dist/webpack-plugin/index.d.cts +0 -5
  121. package/dist/webpack-plugin/index.d.ts +0 -5
  122. package/dist/webpack-plugin/index.js +0 -23
  123. package/dist/webpack-plugin/index.js.map +0 -1
  124. package/dist/webpack-plugin/loader.cjs +0 -155
  125. package/dist/webpack-plugin/loader.cjs.map +0 -1
  126. package/dist/webpack-plugin/loader.d.cts +0 -3
  127. package/dist/webpack-plugin/loader.d.ts +0 -3
  128. package/dist/webpack-plugin/loader.js +0 -122
  129. package/dist/webpack-plugin/loader.js.map +0 -1
  130. /package/{builders.json → packages/angular-grab/builders.json} +0 -0
  131. /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
  132. /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
+ });