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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { filterAngularClasses, type ElementContext, type ComponentStackEntry } from '../../core';
|
|
2
|
+
import { resolveComponent } from './component-resolver';
|
|
3
|
+
import { resolveSource, resolveSourceForComponent } from './source-resolver';
|
|
4
|
+
|
|
5
|
+
const MAX_HTML_LENGTH = 2000;
|
|
6
|
+
|
|
7
|
+
export function buildContext(element: Element): ElementContext {
|
|
8
|
+
const compResult = resolveComponent(element);
|
|
9
|
+
const { filePath, line, column } = resolveSource(element);
|
|
10
|
+
const html = extractHtml(element);
|
|
11
|
+
const selector = generateSelector(element);
|
|
12
|
+
|
|
13
|
+
const cssClasses = filterAngularClasses(element.classList);
|
|
14
|
+
|
|
15
|
+
const componentStack: ComponentStackEntry[] = compResult.stack.map((entry) => {
|
|
16
|
+
const src = resolveSourceForComponent(entry.name);
|
|
17
|
+
return {
|
|
18
|
+
name: entry.name,
|
|
19
|
+
filePath: src.filePath,
|
|
20
|
+
line: src.line,
|
|
21
|
+
column: src.column,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
element,
|
|
27
|
+
html,
|
|
28
|
+
componentName: compResult.name,
|
|
29
|
+
filePath,
|
|
30
|
+
line,
|
|
31
|
+
column,
|
|
32
|
+
componentStack,
|
|
33
|
+
selector,
|
|
34
|
+
cssClasses,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractHtml(element: Element): string {
|
|
39
|
+
const html = element.outerHTML;
|
|
40
|
+
if (html.length <= MAX_HTML_LENGTH) return html;
|
|
41
|
+
return html.slice(0, MAX_HTML_LENGTH) + '<!-- truncated -->';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateSelector(element: Element): string {
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
let current: Element | null = element;
|
|
47
|
+
|
|
48
|
+
while (current && current !== document.documentElement) {
|
|
49
|
+
let selector = current.tagName.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (current.id) {
|
|
52
|
+
selector += `#${current.id}`;
|
|
53
|
+
parts.unshift(selector);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const className = current.getAttribute('class');
|
|
58
|
+
if (className) {
|
|
59
|
+
const classes = className
|
|
60
|
+
.trim()
|
|
61
|
+
.split(/\s+/)
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.slice(0, 3);
|
|
64
|
+
if (classes.length > 0) {
|
|
65
|
+
selector += '.' + classes.join('.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add nth-of-type if there are sibling elements with the same tag
|
|
70
|
+
const parent = current.parentElement;
|
|
71
|
+
if (parent) {
|
|
72
|
+
const siblings = Array.from(parent.children).filter(
|
|
73
|
+
(s) => s.tagName === current!.tagName
|
|
74
|
+
);
|
|
75
|
+
if (siblings.length > 1) {
|
|
76
|
+
const index = siblings.indexOf(current) + 1;
|
|
77
|
+
selector += `:nth-of-type(${index})`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
parts.unshift(selector);
|
|
82
|
+
current = current.parentElement;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join(' > ');
|
|
86
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface NgDebugApi {
|
|
2
|
+
getComponent<T>(element: Element): T | null;
|
|
3
|
+
getOwningComponent<T>(element: Element): T | null;
|
|
4
|
+
getDirectives(element: Element): any[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getNgApi(): NgDebugApi | null {
|
|
8
|
+
if (typeof window === 'undefined') return null;
|
|
9
|
+
return (window as any).ng || null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function cleanComponentName(name: string): string {
|
|
13
|
+
return name.startsWith('_') ? name.slice(1) : name;
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type NgDebugApi, getNgApi, cleanComponentName } from './ng-utils';
|
|
2
|
+
|
|
3
|
+
interface SourceMapEntry {
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getSourceMap(): Record<string, SourceMapEntry> | null {
|
|
9
|
+
return (globalThis as any).__ANGULAR_GRAB_SOURCE_MAP__ ?? null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveSource(element: Element): {
|
|
13
|
+
filePath: string | null;
|
|
14
|
+
line: number | null;
|
|
15
|
+
column: number | null;
|
|
16
|
+
} {
|
|
17
|
+
const sourceMap = getSourceMap();
|
|
18
|
+
if (!sourceMap) return { filePath: null, line: null, column: null };
|
|
19
|
+
|
|
20
|
+
const ng = getNgApi();
|
|
21
|
+
if (!ng) return { filePath: null, line: null, column: null };
|
|
22
|
+
|
|
23
|
+
// Find the component that owns this element
|
|
24
|
+
const componentName = findComponentName(element, ng);
|
|
25
|
+
if (!componentName) return { filePath: null, line: null, column: null };
|
|
26
|
+
|
|
27
|
+
const entry = sourceMap[componentName];
|
|
28
|
+
if (!entry) return { filePath: null, line: null, column: null };
|
|
29
|
+
|
|
30
|
+
return { filePath: entry.file, line: entry.line, column: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveSourceForComponent(componentName: string): {
|
|
34
|
+
filePath: string | null;
|
|
35
|
+
line: number | null;
|
|
36
|
+
column: number | null;
|
|
37
|
+
} {
|
|
38
|
+
const sourceMap = getSourceMap();
|
|
39
|
+
if (!sourceMap) return { filePath: null, line: null, column: null };
|
|
40
|
+
|
|
41
|
+
const entry = sourceMap[componentName];
|
|
42
|
+
if (!entry) return { filePath: null, line: null, column: null };
|
|
43
|
+
|
|
44
|
+
return { filePath: entry.file, line: entry.line, column: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findComponentName(element: Element, ng: NgDebugApi): string | null {
|
|
48
|
+
// Try direct component on this element
|
|
49
|
+
const direct = ng.getComponent(element);
|
|
50
|
+
if (direct) return cleanComponentName(direct.constructor.name);
|
|
51
|
+
|
|
52
|
+
// Walk up to find owning component
|
|
53
|
+
let current: Element | null = element;
|
|
54
|
+
while (current) {
|
|
55
|
+
const owning = ng.getOwningComponent(current);
|
|
56
|
+
if (owning) return cleanComponentName(owning.constructor.name);
|
|
57
|
+
current = current.parentElement;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..', '..');
|
|
6
|
+
|
|
7
|
+
describe('builders.json', () => {
|
|
8
|
+
const buildersJson = JSON.parse(
|
|
9
|
+
readFileSync(join(PACKAGE_ROOT, 'builders.json'), 'utf8'),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
it('has a builders property', () => {
|
|
13
|
+
expect(buildersJson.builders).toBeDefined();
|
|
14
|
+
expect(typeof buildersJson.builders).toBe('object');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('defines an application builder', () => {
|
|
18
|
+
const app = buildersJson.builders.application;
|
|
19
|
+
expect(app).toBeDefined();
|
|
20
|
+
expect(app.implementation).toBeDefined();
|
|
21
|
+
expect(app.schema).toBeDefined();
|
|
22
|
+
expect(app.description).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('defines a dev-server builder', () => {
|
|
26
|
+
const devServer = buildersJson.builders['dev-server'];
|
|
27
|
+
expect(devServer).toBeDefined();
|
|
28
|
+
expect(devServer.implementation).toBeDefined();
|
|
29
|
+
expect(devServer.schema).toBeDefined();
|
|
30
|
+
expect(devServer.description).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('application builder points to correct implementation path', () => {
|
|
34
|
+
const app = buildersJson.builders.application;
|
|
35
|
+
expect(app.implementation).toContain('builders/application');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('dev-server builder points to correct implementation path', () => {
|
|
39
|
+
const devServer = buildersJson.builders['dev-server'];
|
|
40
|
+
expect(devServer.implementation).toContain('builders/dev-server');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('package.json', () => {
|
|
45
|
+
const packageJson = JSON.parse(
|
|
46
|
+
readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it('declares builders field pointing to builders.json', () => {
|
|
50
|
+
expect(packageJson.builders).toBe('./builders.json');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('exports builders.json', () => {
|
|
54
|
+
expect(packageJson.exports['./builders.json']).toBe('./builders.json');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('schema files', () => {
|
|
59
|
+
it('application schema is valid JSON', () => {
|
|
60
|
+
const schema = JSON.parse(
|
|
61
|
+
readFileSync(join(PACKAGE_ROOT, 'src', 'builder', 'builders', 'application', 'schema.json'), 'utf8'),
|
|
62
|
+
);
|
|
63
|
+
expect(schema.type).toBe('object');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('dev-server schema is valid JSON', () => {
|
|
67
|
+
const schema = JSON.parse(
|
|
68
|
+
readFileSync(join(PACKAGE_ROOT, 'src', 'builder', 'builders', 'dev-server', 'schema.json'), 'utf8'),
|
|
69
|
+
);
|
|
70
|
+
expect(schema.type).toBe('object');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createBuilder } from '@angular-devkit/architect';
|
|
2
|
+
import { buildApplication } from '@angular/build';
|
|
3
|
+
import { angularGrabEsbuildPlugin } from '../../../esbuild-plugin';
|
|
4
|
+
|
|
5
|
+
export default createBuilder(async function* (options: any, context) {
|
|
6
|
+
const isProduction = !!options.optimization;
|
|
7
|
+
yield* buildApplication(options, context, {
|
|
8
|
+
codePlugins: [angularGrabEsbuildPlugin({
|
|
9
|
+
rootDir: context.workspaceRoot,
|
|
10
|
+
enabled: !isProduction,
|
|
11
|
+
})],
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createBuilder } from '@angular-devkit/architect';
|
|
2
|
+
import { executeDevServerBuilder } from '@angular/build';
|
|
3
|
+
import { angularGrabEsbuildPlugin } from '../../../esbuild-plugin';
|
|
4
|
+
|
|
5
|
+
export default createBuilder(async function* (options: any, context) {
|
|
6
|
+
yield* executeDevServerBuilder(options, context, {
|
|
7
|
+
buildPlugins: [angularGrabEsbuildPlugin({ rootDir: context.workspaceRoot })],
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -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
|
+
}
|