angular-grab 0.1.1 → 0.1.3
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/.claude/settings.local.json +10 -0
- package/.playwright-mcp/console-2026-03-07T02-49-38-061Z.log +37 -0
- package/.playwright-mcp/console-2026-03-07T02-51-03-493Z.log +26 -0
- package/.playwright-mcp/console-2026-03-07T02-51-25-431Z.log +15 -0
- package/.playwright-mcp/console-2026-03-07T02-52-02-980Z.log +199 -0
- package/.playwright-mcp/page-2026-03-07T02-52-09-791Z.png +0 -0
- 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 +79 -0
- package/examples/angular-19-app/package.json +42 -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
package/package.json
CHANGED
|
@@ -1,119 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "angular-grab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Grab any element in your Angular app and give it to AI coding agents",
|
|
5
|
-
"author": "Nate Richardson",
|
|
5
|
+
"author": "Nate Richardson <hello@naterichardson.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"access": "public"
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
10
9
|
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"exports": {
|
|
19
|
-
".": {
|
|
20
|
-
"types": "./dist/core/index.d.ts",
|
|
21
|
-
"import": "./dist/core/index.js",
|
|
22
|
-
"require": "./dist/core/index.cjs"
|
|
23
|
-
},
|
|
24
|
-
"./global": "./dist/core/index.global.js",
|
|
25
|
-
"./angular": {
|
|
26
|
-
"types": "./dist/angular/index.d.ts",
|
|
27
|
-
"import": "./dist/angular/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./esbuild": {
|
|
30
|
-
"types": "./dist/esbuild-plugin/index.d.ts",
|
|
31
|
-
"import": "./dist/esbuild-plugin/index.js",
|
|
32
|
-
"require": "./dist/esbuild-plugin/index.cjs"
|
|
33
|
-
},
|
|
34
|
-
"./vite": {
|
|
35
|
-
"types": "./dist/vite-plugin/index.d.ts",
|
|
36
|
-
"import": "./dist/vite-plugin/index.js"
|
|
37
|
-
},
|
|
38
|
-
"./webpack": {
|
|
39
|
-
"types": "./dist/webpack-plugin/index.d.ts",
|
|
40
|
-
"import": "./dist/webpack-plugin/index.js",
|
|
41
|
-
"require": "./dist/webpack-plugin/index.cjs"
|
|
42
|
-
},
|
|
43
|
-
"./webpack/loader": {
|
|
44
|
-
"import": "./dist/webpack-plugin/loader.js",
|
|
45
|
-
"require": "./dist/webpack-plugin/loader.cjs"
|
|
46
|
-
},
|
|
47
|
-
"./builder": {
|
|
48
|
-
"require": "./dist/builder/index.js"
|
|
49
|
-
},
|
|
50
|
-
"./builders.json": "./builders.json",
|
|
51
|
-
"./package.json": "./package.json"
|
|
52
|
-
},
|
|
53
|
-
"files": [
|
|
54
|
-
"dist",
|
|
55
|
-
"builders.json"
|
|
56
|
-
],
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"@angular/core": ">=19.0.0",
|
|
59
|
-
"@angular/build": ">=18.0.0",
|
|
60
|
-
"@angular-devkit/architect": ">=0.1800.0",
|
|
61
|
-
"esbuild": ">=0.17.0",
|
|
62
|
-
"typescript": ">=5.0.0",
|
|
63
|
-
"vite": ">=5.0.0",
|
|
64
|
-
"webpack": ">=5.0.0"
|
|
65
|
-
},
|
|
66
|
-
"peerDependenciesMeta": {
|
|
67
|
-
"@angular/core": {
|
|
68
|
-
"optional": true
|
|
69
|
-
},
|
|
70
|
-
"@angular/build": {
|
|
71
|
-
"optional": true
|
|
72
|
-
},
|
|
73
|
-
"@angular-devkit/architect": {
|
|
74
|
-
"optional": true
|
|
75
|
-
},
|
|
76
|
-
"esbuild": {
|
|
77
|
-
"optional": true
|
|
78
|
-
},
|
|
79
|
-
"vite": {
|
|
80
|
-
"optional": true
|
|
81
|
-
},
|
|
82
|
-
"webpack": {
|
|
83
|
-
"optional": true
|
|
84
|
-
}
|
|
10
|
+
"packageManager": "pnpm@9.15.0",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "turbo run build",
|
|
13
|
+
"dev": "turbo run dev",
|
|
14
|
+
"test": "turbo run test",
|
|
15
|
+
"lint": "turbo run lint",
|
|
16
|
+
"clean": "turbo run clean"
|
|
85
17
|
},
|
|
86
18
|
"devDependencies": {
|
|
87
|
-
"@
|
|
88
|
-
"
|
|
89
|
-
"@angular-devkit/architect": "^0.2101.1",
|
|
90
|
-
"esbuild": "^0.21.0",
|
|
91
|
-
"tsup": "^8.0.0",
|
|
92
|
-
"typescript": "^5.4.0",
|
|
93
|
-
"vite": "^5.0.0",
|
|
94
|
-
"vitest": "^2.0.0"
|
|
95
|
-
},
|
|
96
|
-
"repository": {
|
|
97
|
-
"type": "git",
|
|
98
|
-
"url": "https://github.com/naterchrdsn/angular-grab.git",
|
|
99
|
-
"directory": "packages/angular-grab"
|
|
100
|
-
},
|
|
101
|
-
"homepage": "https://github.com/naterchrdsn/angular-grab",
|
|
102
|
-
"bugs": "https://github.com/naterchrdsn/angular-grab/issues",
|
|
103
|
-
"keywords": [
|
|
104
|
-
"angular",
|
|
105
|
-
"angular-grab",
|
|
106
|
-
"element-picker",
|
|
107
|
-
"developer-tools",
|
|
108
|
-
"ai",
|
|
109
|
-
"esbuild",
|
|
110
|
-
"vite",
|
|
111
|
-
"webpack"
|
|
112
|
-
],
|
|
113
|
-
"scripts": {
|
|
114
|
-
"build": "tsup",
|
|
115
|
-
"dev": "tsup --watch",
|
|
116
|
-
"test": "vitest run",
|
|
117
|
-
"clean": "rm -rf dist"
|
|
19
|
+
"@types/node": "^25.3.4",
|
|
20
|
+
"turbo": "^2.8.13"
|
|
118
21
|
}
|
|
119
|
-
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "angular-grab",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Grab any element in your Angular app and give it to AI coding agents",
|
|
5
|
+
"author": "Nate Richardson",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/core/index.cjs",
|
|
12
|
+
"module": "./dist/core/index.js",
|
|
13
|
+
"types": "./dist/core/index.d.ts",
|
|
14
|
+
"bin": {
|
|
15
|
+
"angular-grab": "./dist/cli/index.js"
|
|
16
|
+
},
|
|
17
|
+
"builders": "./builders.json",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/core/index.d.ts",
|
|
21
|
+
"import": "./dist/core/index.js",
|
|
22
|
+
"require": "./dist/core/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./global": "./dist/core/index.global.js",
|
|
25
|
+
"./angular": {
|
|
26
|
+
"types": "./dist/angular/index.d.ts",
|
|
27
|
+
"import": "./dist/angular/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./esbuild": {
|
|
30
|
+
"types": "./dist/esbuild-plugin/index.d.ts",
|
|
31
|
+
"import": "./dist/esbuild-plugin/index.js",
|
|
32
|
+
"require": "./dist/esbuild-plugin/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./vite": {
|
|
35
|
+
"types": "./dist/vite-plugin/index.d.ts",
|
|
36
|
+
"import": "./dist/vite-plugin/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./webpack": {
|
|
39
|
+
"types": "./dist/webpack-plugin/index.d.ts",
|
|
40
|
+
"import": "./dist/webpack-plugin/index.js",
|
|
41
|
+
"require": "./dist/webpack-plugin/index.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./webpack/loader": {
|
|
44
|
+
"import": "./dist/webpack-plugin/loader.js",
|
|
45
|
+
"require": "./dist/webpack-plugin/loader.cjs"
|
|
46
|
+
},
|
|
47
|
+
"./builder": {
|
|
48
|
+
"require": "./dist/builder/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./builders.json": "./builders.json",
|
|
51
|
+
"./package.json": "./package.json"
|
|
52
|
+
},
|
|
53
|
+
"files": ["dist", "builders.json", "README.md"],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"prebuild": "cp ../../README.md .",
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"dev": "tsup --watch",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"clean": "rm -rf dist README.md"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@angular/core": ">=19.0.0",
|
|
63
|
+
"@angular/build": ">=18.0.0",
|
|
64
|
+
"@angular-devkit/architect": ">=0.1800.0",
|
|
65
|
+
"esbuild": ">=0.17.0",
|
|
66
|
+
"typescript": ">=5.0.0",
|
|
67
|
+
"vite": ">=5.0.0",
|
|
68
|
+
"webpack": ">=5.0.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@angular/core": { "optional": true },
|
|
72
|
+
"@angular/build": { "optional": true },
|
|
73
|
+
"@angular-devkit/architect": { "optional": true },
|
|
74
|
+
"esbuild": { "optional": true },
|
|
75
|
+
"vite": { "optional": true },
|
|
76
|
+
"webpack": { "optional": true }
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@angular/core": "^21.0.0",
|
|
80
|
+
"@angular/build": "^21.1.1",
|
|
81
|
+
"@angular-devkit/architect": "^0.2101.1",
|
|
82
|
+
"esbuild": "^0.21.0",
|
|
83
|
+
"tsup": "^8.0.0",
|
|
84
|
+
"typescript": "^5.4.0",
|
|
85
|
+
"vite": "^5.0.0",
|
|
86
|
+
"vitest": "^2.0.0"
|
|
87
|
+
},
|
|
88
|
+
"repository": {
|
|
89
|
+
"type": "git",
|
|
90
|
+
"url": "https://github.com/naterchrdsn/angular-grab.git",
|
|
91
|
+
"directory": "packages/angular-grab"
|
|
92
|
+
},
|
|
93
|
+
"homepage": "https://angular-grab.com",
|
|
94
|
+
"bugs": "https://github.com/naterchrdsn/angular-grab/issues",
|
|
95
|
+
"keywords": ["angular", "angular-grab", "element-picker", "developer-tools", "ai", "esbuild", "vite", "webpack"]
|
|
96
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import type { ComponentStackEntry } from '../../core';
|
|
4
|
+
|
|
5
|
+
vi.mock('../resolvers/component-resolver', () => ({
|
|
6
|
+
resolveComponent: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock('../resolvers/source-resolver', () => ({
|
|
10
|
+
resolveSource: vi.fn(),
|
|
11
|
+
resolveSourceForComponent: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { buildContext } from '../resolvers/context-builder';
|
|
15
|
+
import { resolveComponent } from '../resolvers/component-resolver';
|
|
16
|
+
import { resolveSource, resolveSourceForComponent } from '../resolvers/source-resolver';
|
|
17
|
+
|
|
18
|
+
const mockedResolveComponent = vi.mocked(resolveComponent);
|
|
19
|
+
const mockedResolveSource = vi.mocked(resolveSource);
|
|
20
|
+
const mockedResolveSourceForComponent = vi.mocked(resolveSourceForComponent);
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
|
|
25
|
+
mockedResolveComponent.mockReturnValue({
|
|
26
|
+
name: null,
|
|
27
|
+
hostElement: null,
|
|
28
|
+
stack: [],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
mockedResolveSource.mockReturnValue({
|
|
32
|
+
filePath: null,
|
|
33
|
+
line: null,
|
|
34
|
+
column: null,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
mockedResolveSourceForComponent.mockReturnValue({
|
|
38
|
+
filePath: null,
|
|
39
|
+
line: null,
|
|
40
|
+
column: null,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('buildContext', () => {
|
|
45
|
+
it('produces correct ElementContext shape', () => {
|
|
46
|
+
const el = document.createElement('div');
|
|
47
|
+
el.innerHTML = 'hello';
|
|
48
|
+
|
|
49
|
+
const ctx = buildContext(el);
|
|
50
|
+
|
|
51
|
+
expect(ctx).toEqual(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
element: el,
|
|
54
|
+
html: expect.any(String),
|
|
55
|
+
componentName: null,
|
|
56
|
+
filePath: null,
|
|
57
|
+
line: null,
|
|
58
|
+
column: null,
|
|
59
|
+
componentStack: [],
|
|
60
|
+
selector: expect.any(String),
|
|
61
|
+
cssClasses: expect.any(Array),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes component name from resolver', () => {
|
|
67
|
+
mockedResolveComponent.mockReturnValue({
|
|
68
|
+
name: 'MyComponent',
|
|
69
|
+
hostElement: null,
|
|
70
|
+
stack: [],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const el = document.createElement('div');
|
|
74
|
+
const ctx = buildContext(el);
|
|
75
|
+
|
|
76
|
+
expect(ctx.componentName).toBe('MyComponent');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('includes file path from source resolver', () => {
|
|
80
|
+
mockedResolveSource.mockReturnValue({
|
|
81
|
+
filePath: 'src/app/app.component.ts',
|
|
82
|
+
line: 42,
|
|
83
|
+
column: 3,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const el = document.createElement('div');
|
|
87
|
+
const ctx = buildContext(el);
|
|
88
|
+
|
|
89
|
+
expect(ctx.filePath).toBe('src/app/app.component.ts');
|
|
90
|
+
expect(ctx.line).toBe(42);
|
|
91
|
+
expect(ctx.column).toBe(3);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('builds component stack from resolver stack', () => {
|
|
95
|
+
mockedResolveComponent.mockReturnValue({
|
|
96
|
+
name: 'Child',
|
|
97
|
+
hostElement: null,
|
|
98
|
+
stack: [
|
|
99
|
+
{ name: 'Child', hostElement: null },
|
|
100
|
+
{ name: 'Parent', hostElement: null },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
mockedResolveSourceForComponent
|
|
104
|
+
.mockReturnValueOnce({ filePath: 'child.ts', line: 5, column: null })
|
|
105
|
+
.mockReturnValueOnce({ filePath: 'parent.ts', line: 20, column: null });
|
|
106
|
+
|
|
107
|
+
const el = document.createElement('div');
|
|
108
|
+
const ctx = buildContext(el);
|
|
109
|
+
|
|
110
|
+
expect(ctx.componentStack).toHaveLength(2);
|
|
111
|
+
expect(ctx.componentStack[0]).toEqual({
|
|
112
|
+
name: 'Child',
|
|
113
|
+
filePath: 'child.ts',
|
|
114
|
+
line: 5,
|
|
115
|
+
column: null,
|
|
116
|
+
});
|
|
117
|
+
expect(ctx.componentStack[1]).toEqual({
|
|
118
|
+
name: 'Parent',
|
|
119
|
+
filePath: 'parent.ts',
|
|
120
|
+
line: 20,
|
|
121
|
+
column: null,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('truncates long HTML with truncation marker', () => {
|
|
126
|
+
const el = document.createElement('div');
|
|
127
|
+
// Create content longer than MAX_HTML_LENGTH (2000)
|
|
128
|
+
el.textContent = 'x'.repeat(2100);
|
|
129
|
+
|
|
130
|
+
const ctx = buildContext(el);
|
|
131
|
+
|
|
132
|
+
expect(ctx.html.length).toBeLessThanOrEqual(2000 + '<!-- truncated -->'.length);
|
|
133
|
+
expect(ctx.html).toContain('<!-- truncated -->');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not truncate short HTML', () => {
|
|
137
|
+
const el = document.createElement('div');
|
|
138
|
+
el.textContent = 'short';
|
|
139
|
+
|
|
140
|
+
const ctx = buildContext(el);
|
|
141
|
+
|
|
142
|
+
expect(ctx.html).not.toContain('<!-- truncated -->');
|
|
143
|
+
expect(ctx.html).toContain('short');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('generates CSS selector for element', () => {
|
|
147
|
+
const el = document.createElement('div');
|
|
148
|
+
el.id = 'myid';
|
|
149
|
+
document.body.appendChild(el);
|
|
150
|
+
|
|
151
|
+
const ctx = buildContext(el);
|
|
152
|
+
expect(ctx.selector).toContain('div#myid');
|
|
153
|
+
|
|
154
|
+
document.body.removeChild(el);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('generates selector with classes', () => {
|
|
158
|
+
const el = document.createElement('span');
|
|
159
|
+
el.className = 'foo bar';
|
|
160
|
+
document.body.appendChild(el);
|
|
161
|
+
|
|
162
|
+
const ctx = buildContext(el);
|
|
163
|
+
expect(ctx.selector).toContain('span.foo.bar');
|
|
164
|
+
|
|
165
|
+
document.body.removeChild(el);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('generates selector with nth-of-type for siblings', () => {
|
|
169
|
+
const parent = document.createElement('div');
|
|
170
|
+
const child1 = document.createElement('span');
|
|
171
|
+
const child2 = document.createElement('span');
|
|
172
|
+
parent.appendChild(child1);
|
|
173
|
+
parent.appendChild(child2);
|
|
174
|
+
document.body.appendChild(parent);
|
|
175
|
+
|
|
176
|
+
const ctx = buildContext(child2);
|
|
177
|
+
expect(ctx.selector).toContain('nth-of-type(2)');
|
|
178
|
+
|
|
179
|
+
document.body.removeChild(parent);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('filters out Angular CSS classes', () => {
|
|
183
|
+
const el = document.createElement('div');
|
|
184
|
+
el.classList.add('my-class', 'ng-star-inserted', '_ngcontent-abc', 'real-class');
|
|
185
|
+
|
|
186
|
+
const ctx = buildContext(el);
|
|
187
|
+
|
|
188
|
+
expect(ctx.cssClasses).toContain('my-class');
|
|
189
|
+
expect(ctx.cssClasses).toContain('real-class');
|
|
190
|
+
expect(ctx.cssClasses).not.toContain('ng-star-inserted');
|
|
191
|
+
expect(ctx.cssClasses).not.toContain('_ngcontent-abc');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('handles element with no classes', () => {
|
|
195
|
+
const el = document.createElement('div');
|
|
196
|
+
const ctx = buildContext(el);
|
|
197
|
+
expect(ctx.cssClasses).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('stops selector at element with id', () => {
|
|
201
|
+
const grandparent = document.createElement('section');
|
|
202
|
+
const parent = document.createElement('div');
|
|
203
|
+
parent.id = 'stop-here';
|
|
204
|
+
const child = document.createElement('span');
|
|
205
|
+
grandparent.appendChild(parent);
|
|
206
|
+
parent.appendChild(child);
|
|
207
|
+
document.body.appendChild(grandparent);
|
|
208
|
+
|
|
209
|
+
const ctx = buildContext(child);
|
|
210
|
+
// The selector should start from div#stop-here because id terminates the walk
|
|
211
|
+
expect(ctx.selector).toContain('div#stop-here');
|
|
212
|
+
expect(ctx.selector).toContain('span');
|
|
213
|
+
|
|
214
|
+
document.body.removeChild(grandparent);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { init } from '../core';
|
|
2
|
+
import type { AngularGrabAPI, AngularGrabOptions, Plugin } from '../core';
|
|
3
|
+
import { resolveComponent } from './resolvers/component-resolver';
|
|
4
|
+
import { resolveSource } from './resolvers/source-resolver';
|
|
5
|
+
|
|
6
|
+
declare const ngDevMode: boolean | undefined;
|
|
7
|
+
|
|
8
|
+
let instance: AngularGrabAPI | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize angular-grab. Registers Angular-specific component and source
|
|
12
|
+
* resolvers, then returns the API handle. Idempotent — subsequent calls
|
|
13
|
+
* return the same instance.
|
|
14
|
+
*/
|
|
15
|
+
export function initAngularGrab(options?: Partial<AngularGrabOptions>): AngularGrabAPI {
|
|
16
|
+
if (instance) return instance;
|
|
17
|
+
|
|
18
|
+
// No-op in production
|
|
19
|
+
if (options?.devOnly !== false && typeof ngDevMode !== 'undefined' && !ngDevMode) {
|
|
20
|
+
instance = createNoOpApi();
|
|
21
|
+
return instance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
instance = init(options);
|
|
25
|
+
instance.setComponentResolver((el) => resolveComponent(el));
|
|
26
|
+
instance.setSourceResolver((el) => resolveSource(el));
|
|
27
|
+
|
|
28
|
+
return instance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getAngularGrabApi(): AngularGrabAPI | null {
|
|
32
|
+
return instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerAngularGrabPlugin(plugin: Plugin): void {
|
|
36
|
+
instance?.registerPlugin(plugin);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function disposeAngularGrab(): void {
|
|
40
|
+
instance?.dispose();
|
|
41
|
+
instance = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createNoOpApi(): AngularGrabAPI {
|
|
45
|
+
return {
|
|
46
|
+
activate() {},
|
|
47
|
+
deactivate() {},
|
|
48
|
+
toggle() {},
|
|
49
|
+
isActive() { return false; },
|
|
50
|
+
setOptions() {},
|
|
51
|
+
registerPlugin() {},
|
|
52
|
+
unregisterPlugin() {},
|
|
53
|
+
setComponentResolver() {},
|
|
54
|
+
setSourceResolver() {},
|
|
55
|
+
showToolbar() {},
|
|
56
|
+
hideToolbar() {},
|
|
57
|
+
setThemeMode() {},
|
|
58
|
+
getHistory() { return []; },
|
|
59
|
+
clearHistory() {},
|
|
60
|
+
dispose() {},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Resolvers
|
|
2
|
+
export { resolveComponent } from './resolvers/component-resolver';
|
|
3
|
+
export { resolveSource } from './resolvers/source-resolver';
|
|
4
|
+
export { buildContext } from './resolvers/context-builder';
|
|
5
|
+
|
|
6
|
+
// Angular integration
|
|
7
|
+
export {
|
|
8
|
+
initAngularGrab,
|
|
9
|
+
getAngularGrabApi,
|
|
10
|
+
registerAngularGrabPlugin,
|
|
11
|
+
disposeAngularGrab,
|
|
12
|
+
} from './angular-grab.service';
|
|
13
|
+
export { provideAngularGrab, ANGULAR_GRAB_API } from './provide-angular-grab';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InjectionToken,
|
|
3
|
+
makeEnvironmentProviders,
|
|
4
|
+
provideEnvironmentInitializer,
|
|
5
|
+
type EnvironmentProviders,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
import type { AngularGrabOptions, AngularGrabAPI } from '../core';
|
|
8
|
+
import { initAngularGrab } from './angular-grab.service';
|
|
9
|
+
|
|
10
|
+
export const ANGULAR_GRAB_API = new InjectionToken<AngularGrabAPI>('ANGULAR_GRAB_API');
|
|
11
|
+
|
|
12
|
+
export function provideAngularGrab(
|
|
13
|
+
options?: Partial<AngularGrabOptions>,
|
|
14
|
+
): EnvironmentProviders {
|
|
15
|
+
return makeEnvironmentProviders([
|
|
16
|
+
{
|
|
17
|
+
provide: ANGULAR_GRAB_API,
|
|
18
|
+
useFactory: () => initAngularGrab(options),
|
|
19
|
+
},
|
|
20
|
+
provideEnvironmentInitializer(() => initAngularGrab(options)),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getNgApi, cleanComponentName } from './ng-utils';
|
|
2
|
+
|
|
3
|
+
export function resolveComponent(element: Element): {
|
|
4
|
+
name: string | null;
|
|
5
|
+
hostElement: Element | null;
|
|
6
|
+
stack: Array<{ name: string; hostElement: Element | null }>;
|
|
7
|
+
} {
|
|
8
|
+
const ng = getNgApi();
|
|
9
|
+
if (!ng) return { name: null, hostElement: null, stack: [] };
|
|
10
|
+
|
|
11
|
+
const stack: Array<{ name: string; hostElement: Element | null }> = [];
|
|
12
|
+
const seen = new Set<any>();
|
|
13
|
+
|
|
14
|
+
// Walk up the DOM collecting every Angular component
|
|
15
|
+
let current: Element | null = element;
|
|
16
|
+
while (current) {
|
|
17
|
+
const comp = ng.getComponent(current);
|
|
18
|
+
if (comp && !seen.has(comp)) {
|
|
19
|
+
seen.add(comp);
|
|
20
|
+
stack.push({
|
|
21
|
+
name: cleanComponentName(comp.constructor.name),
|
|
22
|
+
hostElement: current,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
current = current.parentElement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If no direct component found, try getOwningComponent for the original element
|
|
29
|
+
if (stack.length === 0) {
|
|
30
|
+
let walk: Element | null = element;
|
|
31
|
+
while (walk) {
|
|
32
|
+
const owning = ng.getOwningComponent(walk);
|
|
33
|
+
if (owning && !seen.has(owning)) {
|
|
34
|
+
seen.add(owning);
|
|
35
|
+
// Find the host element
|
|
36
|
+
let host: Element | null = walk.parentElement;
|
|
37
|
+
while (host) {
|
|
38
|
+
if (ng.getComponent(host) === owning) break;
|
|
39
|
+
host = host.parentElement;
|
|
40
|
+
}
|
|
41
|
+
stack.push({
|
|
42
|
+
name: cleanComponentName(owning.constructor.name),
|
|
43
|
+
hostElement: host ?? walk.parentElement,
|
|
44
|
+
});
|
|
45
|
+
// Continue walking from the host to collect parents
|
|
46
|
+
current = host?.parentElement ?? walk.parentElement?.parentElement ?? null;
|
|
47
|
+
while (current) {
|
|
48
|
+
const comp = ng.getComponent(current);
|
|
49
|
+
if (comp && !seen.has(comp)) {
|
|
50
|
+
seen.add(comp);
|
|
51
|
+
stack.push({
|
|
52
|
+
name: cleanComponentName(comp.constructor.name),
|
|
53
|
+
hostElement: current,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
current = current.parentElement;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
walk = walk.parentElement;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const closest = stack.length > 0 ? stack[0] : { name: null, hostElement: null };
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name: closest.name,
|
|
68
|
+
hostElement: closest.hostElement,
|
|
69
|
+
stack,
|
|
70
|
+
};
|
|
71
|
+
}
|