ngx-locatorjs 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +61 -40
- package/README.md +51 -23
- package/dist/browser/auto.js +1 -1
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +28 -3
- package/dist/node/cmp-scan.js +80 -56
- package/dist/node/config-setup.js +12 -53
- package/dist/node/file-opener.js +197 -45
- package/package.json +23 -30
package/README.ko.md
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# ngx-locatorjs (Open-in-Editor)
|
|
2
2
|
|
|
3
3
|
브라우저에서 Alt+클릭으로 Angular 컴포넌트 파일을 에디터에서 바로 여는 개발용 도구입니다. Angular 프로젝트 어디에나 npm 패키지로 설치해 사용할 수 있습니다.
|
|
4
|
+
이 프로젝트는 [locatorjs.com](https://www.locatorjs.com/)에서 영감을 받았습니다.
|
|
4
5
|
|
|
5
6
|
**기능**
|
|
7
|
+
|
|
6
8
|
- Alt+클릭: 템플릿(.html) 열기
|
|
7
9
|
- Alt+Shift+클릭: 컴포넌트(.ts) 열기
|
|
8
10
|
- Alt 키 홀드: 컴포넌트 하이라이트 + 툴팁 표시
|
|
9
11
|
- Cursor, VS Code, WebStorm 지원
|
|
10
12
|
|
|
11
|
-
**필수 단계 (1~
|
|
13
|
+
**필수 단계 (1~4 반드시 수행)**
|
|
14
|
+
|
|
12
15
|
1. 패키지 설치: `npm i -D ngx-locatorjs`
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
2. 설정/프록시 생성: `npx locatorjs-config`
|
|
17
|
+
3. `main.ts`에 런타임 훅 추가 (아래 예시 참고)
|
|
18
|
+
4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
|
|
19
|
+
|
|
20
|
+
`npm run start` 사용 시 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
18
21
|
|
|
19
22
|
**Angular 코드 추가 (main.ts)**
|
|
23
|
+
|
|
20
24
|
```ts
|
|
21
25
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
22
26
|
import { AppModule } from './app/app.module';
|
|
@@ -32,7 +36,7 @@ platformBrowserDynamic()
|
|
|
32
36
|
.then(() => {
|
|
33
37
|
if (!environment.production) {
|
|
34
38
|
setTimeout(() => {
|
|
35
|
-
import('ngx-locatorjs').then((m) => m.installAngularLocator());
|
|
39
|
+
import('ngx-locatorjs').then((m) => m.installAngularLocator({ enableNetwork: true }));
|
|
36
40
|
}, 1000);
|
|
37
41
|
}
|
|
38
42
|
})
|
|
@@ -40,6 +44,7 @@ platformBrowserDynamic()
|
|
|
40
44
|
```
|
|
41
45
|
|
|
42
46
|
**Angular 코드 추가 (standalone: bootstrapApplication)**
|
|
47
|
+
|
|
43
48
|
```ts
|
|
44
49
|
import { bootstrapApplication } from '@angular/platform-browser';
|
|
45
50
|
import { appConfig } from './app/app.config';
|
|
@@ -49,7 +54,7 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
49
54
|
.then(() => {
|
|
50
55
|
setTimeout(() => {
|
|
51
56
|
import('ngx-locatorjs')
|
|
52
|
-
.then((m) => m.installAngularLocator())
|
|
57
|
+
.then((m) => m.installAngularLocator({ enableNetwork: true }))
|
|
53
58
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
54
59
|
}, 1000);
|
|
55
60
|
})
|
|
@@ -57,38 +62,43 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
57
62
|
```
|
|
58
63
|
|
|
59
64
|
**Angular dev server 예시**
|
|
65
|
+
|
|
60
66
|
- CLI 실행
|
|
61
|
-
`ng serve --proxy-config ngx-locatorjs.proxy.json`
|
|
67
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.json`
|
|
62
68
|
|
|
63
69
|
- angular.json에 적용
|
|
64
|
-
`"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.json"` 추가
|
|
70
|
+
`"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.json"` 추가
|
|
65
71
|
|
|
66
72
|
**컴포넌트 맵 스캔**
|
|
73
|
+
|
|
67
74
|
- 수동 스캔
|
|
68
|
-
`npx locatorjs-scan`
|
|
75
|
+
`npx locatorjs-scan`
|
|
69
76
|
|
|
70
77
|
- 변경 감지 자동 스캔(선택)
|
|
71
|
-
`nodemon --delay 2.5 -e ts,html -w src -w projects -w apps -w libs -x "npx locatorjs-scan"`
|
|
78
|
+
`nodemon --delay 2.5 -e ts,html -w src -w projects -w apps -w libs -x "npx locatorjs-scan"`
|
|
72
79
|
|
|
73
80
|
**가능한 것**
|
|
81
|
+
|
|
74
82
|
- Alt+클릭으로 템플릿 또는 컴포넌트 파일 열기 (개발 모드)
|
|
75
83
|
- Alt 키 홀드 시 컴포넌트 하이라이트 및 툴팁 표시
|
|
76
84
|
- 단일 Angular 앱, workspace, Nx 구조에서 동작
|
|
77
85
|
|
|
78
86
|
**불가능/제한 사항**
|
|
79
|
-
|
|
87
|
+
|
|
80
88
|
- SSR/SSG 환경에서는 동작하지 않음 (브라우저 DOM 기반)
|
|
81
89
|
|
|
82
90
|
**ngx-locatorjs.config.json 가이드**
|
|
83
91
|
파일 위치: 프로젝트 루트
|
|
84
92
|
|
|
85
93
|
**중요**
|
|
94
|
+
|
|
86
95
|
- `npx locatorjs-config`는 **실행한 현재 폴더를 기준**으로 설정합니다.
|
|
87
|
-
-
|
|
88
|
-
- 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 그 **상대
|
|
96
|
+
- 기본값: `port: 4123`, `workspaceRoot: "."`.
|
|
97
|
+
- 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 `workspaceRoot`를 그 **상대 경로**로 수정하세요. (예: `apps/web`)
|
|
89
98
|
- `.gitignore`가 있으면 `npx locatorjs-config`가 `.open-in-editor/`를 자동 추가합니다. 커밋하려면 해당 항목을 제거하세요.
|
|
90
99
|
|
|
91
100
|
예시:
|
|
101
|
+
|
|
92
102
|
```json
|
|
93
103
|
{
|
|
94
104
|
"port": 4123,
|
|
@@ -116,22 +126,25 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
116
126
|
```
|
|
117
127
|
|
|
118
128
|
**필드 설명**
|
|
119
|
-
|
|
120
|
-
- `
|
|
121
|
-
- `
|
|
122
|
-
- `
|
|
123
|
-
- `
|
|
124
|
-
- `scan.
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
|
|
130
|
+
- `port`: 로컬 file-opener 서버 포트입니다.
|
|
131
|
+
- `workspaceRoot`: 명령 실행 위치 기준 Angular 워크스페이스 루트 상대 경로입니다.
|
|
132
|
+
- `editor`: 기본 에디터입니다 (`cursor`, `code`, `webstorm`).
|
|
133
|
+
- `fallbackEditor`: 기본 에디터 실행 실패 시 사용할 대체 에디터입니다.
|
|
134
|
+
- `scan.includeGlobs`: 컴포넌트 소스 파일 탐색 대상 glob 목록입니다.
|
|
135
|
+
- `scan.excludeGlobs`: 컴포넌트 스캔에서 제외할 glob 목록입니다.
|
|
136
|
+
|
|
137
|
+
**프로젝트 구조별 includeGlobs 예시**
|
|
138
|
+
|
|
127
139
|
1. 일반 Angular 앱
|
|
128
|
-
`["src/app/**/*.ts"]`
|
|
140
|
+
`["src/app/**/*.ts"]`
|
|
129
141
|
2. Angular Workspace (projects/)
|
|
130
|
-
`["projects/**/*.{ts,tsx}"]`
|
|
142
|
+
`["projects/**/*.{ts,tsx}"]`
|
|
131
143
|
3. Nx (apps/libs)
|
|
132
|
-
`["apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"]`
|
|
144
|
+
`["apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"]`
|
|
133
145
|
|
|
134
146
|
**환경변수 우선순위**
|
|
147
|
+
|
|
135
148
|
1. `EDITOR_CMD` 예: `EDITOR_CMD="cursor --goto"`
|
|
136
149
|
2. `LAUNCH_EDITOR` 예: `LAUNCH_EDITOR=code`
|
|
137
150
|
3. `ngx-locatorjs.config.json`의 `editor`
|
|
@@ -141,6 +154,7 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
141
154
|
`npx locatorjs-config` 실행 시 자동 생성됩니다. `angular.json`에 지정된 proxyConfig나 `proxy.conf.json`이 있으면 그 파일에 병합됩니다. 없으면 `ngx-locatorjs.proxy.json`을 생성합니다.
|
|
142
155
|
|
|
143
156
|
예시:
|
|
157
|
+
|
|
144
158
|
```json
|
|
145
159
|
{
|
|
146
160
|
"/__open-in-editor": {
|
|
@@ -162,30 +176,36 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
162
176
|
```
|
|
163
177
|
|
|
164
178
|
**트러블슈팅**
|
|
179
|
+
|
|
165
180
|
1. CORS 에러
|
|
166
|
-
`ng serve --proxy-config ngx-locatorjs.proxy.json` 사용 여부 확인
|
|
181
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.json` 사용 여부 확인
|
|
167
182
|
2. npm run 경고
|
|
168
|
-
`npm run start -- --proxy-config ngx-locatorjs.proxy.json` 형태로 실행
|
|
169
|
-
3.
|
|
170
|
-
`
|
|
171
|
-
4.
|
|
172
|
-
`scan.
|
|
173
|
-
5.
|
|
174
|
-
`
|
|
175
|
-
6.
|
|
176
|
-
|
|
177
|
-
7.
|
|
178
|
-
|
|
179
|
-
8.
|
|
180
|
-
`
|
|
183
|
+
`npm run start -- --proxy-config ngx-locatorjs.proxy.json` 형태로 실행
|
|
184
|
+
3. 네트워크 비활성
|
|
185
|
+
`installAngularLocator({ enableNetwork: true })` 설정 확인
|
|
186
|
+
4. component-map.json not found
|
|
187
|
+
`npx locatorjs-scan` 실행 후 `.open-in-editor/component-map.json` 생성 여부 확인
|
|
188
|
+
5. 컴포넌트 변경이 반영되지 않음
|
|
189
|
+
`npx locatorjs-open-in-editor --watch` 사용 또는 `npx locatorjs-scan` 재실행
|
|
190
|
+
6. 스캔 결과가 비어있거나 컴포넌트가 누락됨
|
|
191
|
+
`scan.includeGlobs` 경로 확인 후 재스캔. 실제 컴포넌트들이 위치한 경로를 입력해야 합니다.
|
|
192
|
+
7. 잘못된 파일이 열리거나 매칭이 안 됨
|
|
193
|
+
`workspaceRoot`가 Angular 앱 루트인지 확인
|
|
194
|
+
8. 하이라이트가 안 보이거나 info가 null로 나옴
|
|
195
|
+
`http://localhost:${port}/__cmp-map` 에서 컴포넌트 정보가 잘 나타나는지 확인
|
|
196
|
+
9. 포트 충돌
|
|
197
|
+
`ngx-locatorjs.config.json`과 `ngx-locatorjs.proxy.json`에서 포트 일치 여부 확인
|
|
181
198
|
|
|
182
199
|
**주의**
|
|
200
|
+
|
|
183
201
|
- 개발 모드에서만 사용하세요. 프로덕션 번들에 포함되지 않도록 `environment.production` 체크를 권장합니다.
|
|
202
|
+
- 네트워크 요청은 opt-in이며 localhost로만 제한됩니다. `enableNetwork: true`로 활성화하세요.
|
|
184
203
|
|
|
185
204
|
**원 커맨드 실행 (추천)**
|
|
186
205
|
file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식 중 하나를 사용하세요.
|
|
187
206
|
|
|
188
207
|
### Option A: `concurrently`
|
|
208
|
+
|
|
189
209
|
```bash
|
|
190
210
|
npm i -D concurrently
|
|
191
211
|
```
|
|
@@ -199,6 +219,7 @@ npm i -D concurrently
|
|
|
199
219
|
```
|
|
200
220
|
|
|
201
221
|
### Option B: `npm-run-all`
|
|
222
|
+
|
|
202
223
|
```bash
|
|
203
224
|
npm i -D npm-run-all
|
|
204
225
|
```
|
package/README.md
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
# ngx-locatorjs (Angular Open-in-Editor)
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
한국어 문서: [README.ko.md](README.ko.md)
|
|
4
6
|
|
|
5
7
|
Open Angular component files directly from the browser with **Alt + Click** during development. This package provides:
|
|
8
|
+
|
|
6
9
|
- Browser runtime for Alt+click / hover UI
|
|
7
10
|
- CLI tools to scan Angular components and open files in your editor
|
|
8
11
|
- Config + proxy setup guidance
|
|
9
12
|
|
|
13
|
+
Inspired by [locatorjs.com](https://www.locatorjs.com/).
|
|
14
|
+
|
|
10
15
|
## Features
|
|
16
|
+
|
|
11
17
|
- **Alt + Click**: open template (.html)
|
|
12
18
|
- **Alt + Shift + Click**: open component (.ts)
|
|
13
19
|
- **Hold Alt**: highlight component + tooltip
|
|
14
20
|
- Supports **Cursor**, **VS Code**, **WebStorm**
|
|
15
21
|
|
|
16
22
|
## Install
|
|
23
|
+
|
|
17
24
|
```bash
|
|
18
25
|
npm i -D ngx-locatorjs
|
|
19
26
|
```
|
|
20
27
|
|
|
21
28
|
## Required Steps (Do This First)
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
You must complete steps 1–4 for this to work.
|
|
23
31
|
|
|
24
32
|
1. Install the package: `npm i -D ngx-locatorjs`
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
2. Generate config + proxy: `npx locatorjs-config`
|
|
34
|
+
3. Add the runtime hook to `main.ts` (see the examples below)
|
|
35
|
+
4. Run the file-opener server and your dev server (keep both running): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
|
|
36
|
+
|
|
37
|
+
If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
30
38
|
|
|
31
39
|
## Add to `main.ts`
|
|
32
40
|
|
|
33
41
|
### NgModule bootstrap
|
|
42
|
+
|
|
34
43
|
```ts
|
|
35
44
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
36
45
|
import { AppModule } from './app/app.module';
|
|
@@ -47,7 +56,9 @@ platformBrowserDynamic()
|
|
|
47
56
|
if (!environment.production) {
|
|
48
57
|
setTimeout(() => {
|
|
49
58
|
import('ngx-locatorjs')
|
|
50
|
-
.then(
|
|
59
|
+
.then(
|
|
60
|
+
(m) => m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
|
|
61
|
+
)
|
|
51
62
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
52
63
|
}, 1000);
|
|
53
64
|
}
|
|
@@ -56,6 +67,7 @@ platformBrowserDynamic()
|
|
|
56
67
|
```
|
|
57
68
|
|
|
58
69
|
### Standalone bootstrap
|
|
70
|
+
|
|
59
71
|
```ts
|
|
60
72
|
import { bootstrapApplication } from '@angular/platform-browser';
|
|
61
73
|
import { appConfig } from './app/app.config';
|
|
@@ -65,7 +77,9 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
65
77
|
.then(() => {
|
|
66
78
|
setTimeout(() => {
|
|
67
79
|
import('ngx-locatorjs')
|
|
68
|
-
.then(
|
|
80
|
+
.then(
|
|
81
|
+
(m) => m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
|
|
82
|
+
)
|
|
69
83
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
70
84
|
}, 1000);
|
|
71
85
|
})
|
|
@@ -73,15 +87,18 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
73
87
|
```
|
|
74
88
|
|
|
75
89
|
## Config Guide (`ngx-locatorjs.config.json`)
|
|
90
|
+
|
|
76
91
|
Location: project root
|
|
77
92
|
|
|
78
93
|
**Important**
|
|
94
|
+
|
|
79
95
|
- `npx locatorjs-config` uses the **current directory** as the base.
|
|
80
|
-
-
|
|
81
|
-
- In a monorepo,
|
|
96
|
+
- Defaults: `port: 4123`, `workspaceRoot: "."`.
|
|
97
|
+
- In a monorepo, update `workspaceRoot` to the **relative path** of your Angular app (e.g. `apps/web`).
|
|
82
98
|
- If `.gitignore` exists, `npx locatorjs-config` will append `.open-in-editor/`. Remove it if you want to commit the map.
|
|
83
99
|
|
|
84
100
|
Example:
|
|
101
|
+
|
|
85
102
|
```json
|
|
86
103
|
{
|
|
87
104
|
"port": 4123,
|
|
@@ -109,22 +126,26 @@ Example:
|
|
|
109
126
|
```
|
|
110
127
|
|
|
111
128
|
### Field Reference
|
|
112
|
-
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
116
|
-
- `
|
|
117
|
-
- `scan.
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
|
|
130
|
+
- `port`: Port for the local file-opener server.
|
|
131
|
+
- `workspaceRoot`: Angular workspace root path (relative to where you run commands).
|
|
132
|
+
- `editor`: Preferred editor (`cursor`, `code`, `webstorm`).
|
|
133
|
+
- `fallbackEditor`: Fallback editor if the preferred editor cannot be launched.
|
|
134
|
+
- `scan.includeGlobs`: Globs used to find component source files.
|
|
135
|
+
- `scan.excludeGlobs`: Globs excluded from component scanning.
|
|
136
|
+
|
|
137
|
+
### Example includeGlobs
|
|
138
|
+
|
|
120
139
|
- Simple app: `"src/app/**/*.ts"`
|
|
121
140
|
- Angular workspace: `"projects/**/*.{ts,tsx}"`
|
|
122
141
|
- Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
|
|
123
142
|
|
|
124
143
|
## Proxy (`ngx-locatorjs.proxy.json`)
|
|
144
|
+
|
|
125
145
|
Generated by `npx locatorjs-config`. If a proxy file is already referenced in `angular.json` or `proxy.conf.json` exists, it will merge entries there. Otherwise it creates `ngx-locatorjs.proxy.json`.
|
|
126
146
|
|
|
127
147
|
Example:
|
|
148
|
+
|
|
128
149
|
```json
|
|
129
150
|
{
|
|
130
151
|
"/__open-in-editor": {
|
|
@@ -146,28 +167,35 @@ Example:
|
|
|
146
167
|
```
|
|
147
168
|
|
|
148
169
|
## Environment Variable Priority
|
|
170
|
+
|
|
149
171
|
1. `EDITOR_CMD` (example: `cursor --goto`)
|
|
150
172
|
2. `LAUNCH_EDITOR` (example: `code`)
|
|
151
173
|
3. `ngx-locatorjs.config.json` → `editor`
|
|
152
174
|
4. auto-detected editor
|
|
153
175
|
|
|
154
176
|
## Troubleshooting
|
|
177
|
+
|
|
155
178
|
- **CORS / JSON parse error**: ensure dev server uses `--proxy-config ngx-locatorjs.proxy.json`
|
|
156
179
|
- **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
180
|
+
- **Network disabled**: pass `enableNetwork: true` to `installAngularLocator`
|
|
157
181
|
- **component-map.json not found**: run `npx locatorjs-scan`
|
|
182
|
+
- **Component changes not reflected**: run `npx locatorjs-open-in-editor --watch` or re-run `npx locatorjs-scan`
|
|
158
183
|
- **Map is empty or missing components**: check `scan.includeGlobs` and rerun the scan
|
|
159
184
|
- **Wrong files open or nothing matches**: confirm `workspaceRoot` points to the actual Angular app root
|
|
160
|
-
- **No highlight / info is null**: make sure
|
|
161
|
-
- **Editor not opening**: install editor CLI or set `EDITOR_CMD`
|
|
185
|
+
- **No highlight / info is null**: make sure `http://localhost:${port}/__cmp-map` is loading and includes your component class name
|
|
162
186
|
- **Port conflict**: change port in both `ngx-locatorjs.config.json` and `ngx-locatorjs.proxy.json`
|
|
163
187
|
|
|
164
188
|
## Notes
|
|
189
|
+
|
|
165
190
|
- Use only in development (guard with `environment.production`).
|
|
191
|
+
- Network requests are opt-in and limited to localhost. Set `enableNetwork: true` to activate.
|
|
166
192
|
|
|
167
193
|
## One-Command Dev (Recommended)
|
|
194
|
+
|
|
168
195
|
Running the file-opener server and Angular dev server separately is tedious. You can wire them into a single script.
|
|
169
196
|
|
|
170
197
|
### Option A: `concurrently`
|
|
198
|
+
|
|
171
199
|
```bash
|
|
172
200
|
npm i -D concurrently
|
|
173
201
|
```
|
|
@@ -181,6 +209,7 @@ npm i -D concurrently
|
|
|
181
209
|
```
|
|
182
210
|
|
|
183
211
|
### Option B: `npm-run-all`
|
|
212
|
+
|
|
184
213
|
```bash
|
|
185
214
|
npm i -D npm-run-all
|
|
186
215
|
```
|
|
@@ -196,12 +225,11 @@ npm i -D npm-run-all
|
|
|
196
225
|
```
|
|
197
226
|
|
|
198
227
|
## What It Can Do
|
|
228
|
+
|
|
199
229
|
- Open template or component files with Alt+click in development
|
|
200
230
|
- Show component highlight and tooltip while holding Alt
|
|
201
231
|
- Works with single apps, Angular workspace, and Nx layouts
|
|
202
232
|
|
|
203
233
|
## Limitations
|
|
204
|
-
|
|
205
|
-
- Requires the file-opener server to be running
|
|
206
|
-
- Line matching in dynamic/repeated templates is heuristic, not perfect
|
|
234
|
+
|
|
207
235
|
- Not supported in SSR/SSG runtime (browser DOM only)
|
package/dist/browser/auto.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { installAngularLocator } from './index';
|
|
2
|
-
installAngularLocator();
|
|
2
|
+
installAngularLocator({ enableNetwork: true });
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAChD,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAChD,CAAC;AAkBF,MAAM,MAAM,uBAAuB,GAAG;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AA4EF,iBAAe,SAAS,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B9D;AA+eD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,iBAa9E;AAED,wBAAsB,mBAAmB,kBASxC;AAED,wBAAsB,mBAAmB,kBAExC;AAED,wBAAgB,yBAAyB,YAExC;AAED,eAAO,MAAM,SAAS;;CAErB,CAAC"}
|
package/dist/browser/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const DEFAULT_ENDPOINTS = {
|
|
|
5
5
|
};
|
|
6
6
|
const DEFAULT_OPTIONS = {
|
|
7
7
|
endpoints: DEFAULT_ENDPOINTS,
|
|
8
|
+
enableNetwork: false,
|
|
8
9
|
prefetchMap: true,
|
|
9
10
|
enableHover: true,
|
|
10
11
|
enableClick: true,
|
|
@@ -16,6 +17,30 @@ let OPTIONS = DEFAULT_OPTIONS;
|
|
|
16
17
|
let INSTALLED = false;
|
|
17
18
|
let CMP_MAP = null;
|
|
18
19
|
let mapLoadPromise = null;
|
|
20
|
+
const LOCALHOST_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
21
|
+
function isLocalhostHost(hostname) {
|
|
22
|
+
if (LOCALHOST_HOSTS.has(hostname))
|
|
23
|
+
return true;
|
|
24
|
+
if (hostname.endsWith('.localhost'))
|
|
25
|
+
return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function assertNetworkAllowed(url) {
|
|
29
|
+
if (!OPTIONS.enableNetwork) {
|
|
30
|
+
if (OPTIONS.debug) {
|
|
31
|
+
console.warn('[angular-locator] Network is disabled. Set enableNetwork: true.');
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Network disabled');
|
|
34
|
+
}
|
|
35
|
+
const resolved = new URL(url, window.location.href);
|
|
36
|
+
if (!isLocalhostHost(resolved.hostname)) {
|
|
37
|
+
if (OPTIONS.debug) {
|
|
38
|
+
console.warn('[angular-locator] Network is limited to localhost:', resolved.href);
|
|
39
|
+
}
|
|
40
|
+
throw new Error('Network restricted to localhost');
|
|
41
|
+
}
|
|
42
|
+
return resolved.toString();
|
|
43
|
+
}
|
|
19
44
|
function normalizeMap(map) {
|
|
20
45
|
if (!map.filePathsByClassName || Object.keys(map.filePathsByClassName).length === 0) {
|
|
21
46
|
const rebuilt = {};
|
|
@@ -34,7 +59,7 @@ async function ensureMap(forceRefresh = false) {
|
|
|
34
59
|
if (CMP_MAP && !forceRefresh)
|
|
35
60
|
return CMP_MAP;
|
|
36
61
|
const timestamp = Date.now();
|
|
37
|
-
const res = await fetch(`${OPTIONS.endpoints.componentMap}?t=${timestamp}
|
|
62
|
+
const res = await fetch(assertNetworkAllowed(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`), {
|
|
38
63
|
cache: 'no-store',
|
|
39
64
|
headers: {
|
|
40
65
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
@@ -173,7 +198,7 @@ function getNearestComponent(el) {
|
|
|
173
198
|
};
|
|
174
199
|
}
|
|
175
200
|
async function openFile(absPath, line = 1, col = 1) {
|
|
176
|
-
const url = `${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}
|
|
201
|
+
const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`);
|
|
177
202
|
try {
|
|
178
203
|
await fetch(url);
|
|
179
204
|
}
|
|
@@ -184,7 +209,7 @@ async function openFile(absPath, line = 1, col = 1) {
|
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
async function openFileWithSearch(absPath, searchTerms) {
|
|
187
|
-
const url = `${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}
|
|
212
|
+
const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`);
|
|
188
213
|
try {
|
|
189
214
|
await fetch(url);
|
|
190
215
|
}
|
package/dist/node/cmp-scan.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import ts from 'typescript';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { glob } from 'glob';
|
|
5
6
|
const DEFAULT_INCLUDE_GLOBS = [
|
|
6
7
|
'src/**/*.{ts,tsx}',
|
|
7
8
|
'projects/**/*.{ts,tsx}',
|
|
@@ -31,28 +32,71 @@ function readConfig() {
|
|
|
31
32
|
function toPosix(p) {
|
|
32
33
|
return p.replace(/\\/g, '/');
|
|
33
34
|
}
|
|
34
|
-
function prefixWorkspaceRoot(
|
|
35
|
+
function prefixWorkspaceRoot(globPattern, workspaceRoot) {
|
|
35
36
|
if (!workspaceRoot || workspaceRoot === '.' || workspaceRoot === './')
|
|
36
|
-
return
|
|
37
|
-
if (path.isAbsolute(
|
|
38
|
-
return
|
|
37
|
+
return globPattern;
|
|
38
|
+
if (path.isAbsolute(globPattern))
|
|
39
|
+
return globPattern;
|
|
39
40
|
const rootPosix = toPosix(workspaceRoot).replace(/\/+$/, '');
|
|
40
|
-
const globPosix = toPosix(
|
|
41
|
+
const globPosix = toPosix(globPattern).replace(/^\/+/, '');
|
|
41
42
|
if (globPosix.startsWith(rootPosix + '/'))
|
|
42
43
|
return globPosix;
|
|
43
44
|
return `${rootPosix}/${globPosix}`;
|
|
44
45
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
46
|
+
function extractTemplateUrl(node) {
|
|
47
|
+
for (const prop of node.properties) {
|
|
48
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
49
|
+
ts.isIdentifier(prop.name) &&
|
|
50
|
+
prop.name.text === 'templateUrl') {
|
|
51
|
+
const initializer = prop.initializer;
|
|
52
|
+
if (ts.isStringLiteral(initializer)) {
|
|
53
|
+
return initializer.text;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function findComponentDecorator(node) {
|
|
60
|
+
if (!node.modifiers)
|
|
61
|
+
return undefined;
|
|
62
|
+
for (const modifier of node.modifiers) {
|
|
63
|
+
if (ts.isDecorator(modifier)) {
|
|
64
|
+
const expr = modifier.expression;
|
|
65
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
66
|
+
if (expr.expression.text === 'Component') {
|
|
67
|
+
return modifier;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
47
73
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
function parseSourceFile(filePath, sourceCode) {
|
|
75
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
|
|
76
|
+
const components = [];
|
|
77
|
+
function visit(node) {
|
|
78
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
79
|
+
const componentDecorator = findComponentDecorator(node);
|
|
80
|
+
if (componentDecorator) {
|
|
81
|
+
const expr = componentDecorator.expression;
|
|
82
|
+
const firstArg = expr.arguments[0];
|
|
83
|
+
if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
84
|
+
const templateUrl = extractTemplateUrl(firstArg);
|
|
85
|
+
const className = node.name.text;
|
|
86
|
+
const absTs = path.resolve(root, filePath);
|
|
87
|
+
const absTpl = templateUrl ? path.resolve(path.dirname(absTs), templateUrl) : undefined;
|
|
88
|
+
components.push({
|
|
89
|
+
className,
|
|
90
|
+
filePath: absTs,
|
|
91
|
+
templateUrl: absTpl,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ts.forEachChild(node, visit);
|
|
97
|
+
}
|
|
98
|
+
visit(sourceFile);
|
|
99
|
+
return components;
|
|
56
100
|
}
|
|
57
101
|
async function main() {
|
|
58
102
|
const cfg = readConfig();
|
|
@@ -88,14 +132,16 @@ async function main() {
|
|
|
88
132
|
}
|
|
89
133
|
return stats;
|
|
90
134
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
const allFiles = [];
|
|
136
|
+
for (const pattern of effectiveIncludeGlobs) {
|
|
137
|
+
const files = await glob(pattern, {
|
|
138
|
+
ignore: excludeGlobs,
|
|
139
|
+
absolute: true,
|
|
140
|
+
cwd: root,
|
|
141
|
+
});
|
|
142
|
+
allFiles.push(...files);
|
|
143
|
+
}
|
|
144
|
+
const filePaths = [...new Set(allFiles)];
|
|
99
145
|
const currentStats = getFileStats(filePaths);
|
|
100
146
|
const previousCache = loadCache();
|
|
101
147
|
const hasChanges = filePaths.some((filePath) => !previousCache[filePath] || previousCache[filePath] !== currentStats[filePath]);
|
|
@@ -108,38 +154,16 @@ async function main() {
|
|
|
108
154
|
}
|
|
109
155
|
const detailByFilePath = {};
|
|
110
156
|
const filePathsByClassName = {};
|
|
111
|
-
for (const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
continue;
|
|
122
|
-
const obj = arg.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
123
|
-
const templateUrlProp = obj.getProperty('templateUrl');
|
|
124
|
-
const templateUrl = templateUrlProp
|
|
125
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
126
|
-
?.getInitializer()
|
|
127
|
-
?.getText()
|
|
128
|
-
.replace(/^`|^'|^"|"|'|`$/g, '');
|
|
129
|
-
const className = cls.getName();
|
|
130
|
-
if (!className)
|
|
131
|
-
continue;
|
|
132
|
-
const absTs = path.resolve(root, filePath);
|
|
133
|
-
const absTpl = templateUrl ? path.resolve(path.dirname(absTs), templateUrl) : undefined;
|
|
134
|
-
detailByFilePath[absTs] = {
|
|
135
|
-
className,
|
|
136
|
-
filePath: absTs,
|
|
137
|
-
templateUrl: absTpl,
|
|
138
|
-
};
|
|
139
|
-
if (!filePathsByClassName[className])
|
|
140
|
-
filePathsByClassName[className] = [];
|
|
141
|
-
if (!filePathsByClassName[className].includes(absTs)) {
|
|
142
|
-
filePathsByClassName[className].push(absTs);
|
|
157
|
+
for (const filePath of filePaths) {
|
|
158
|
+
const sourceCode = fs.readFileSync(filePath, 'utf8');
|
|
159
|
+
const components = parseSourceFile(filePath, sourceCode);
|
|
160
|
+
for (const cmp of components) {
|
|
161
|
+
detailByFilePath[cmp.filePath] = cmp;
|
|
162
|
+
if (!filePathsByClassName[cmp.className]) {
|
|
163
|
+
filePathsByClassName[cmp.className] = [];
|
|
164
|
+
}
|
|
165
|
+
if (!filePathsByClassName[cmp.className].includes(cmp.filePath)) {
|
|
166
|
+
filePathsByClassName[cmp.className].push(cmp.filePath);
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
}
|
|
@@ -32,9 +32,10 @@ else {
|
|
|
32
32
|
}
|
|
33
33
|
async function startSetup() {
|
|
34
34
|
try {
|
|
35
|
+
logDefaults();
|
|
35
36
|
const config = {
|
|
36
|
-
port:
|
|
37
|
-
workspaceRoot:
|
|
37
|
+
port: 4123,
|
|
38
|
+
workspaceRoot: '.',
|
|
38
39
|
editor: await selectEditor(),
|
|
39
40
|
fallbackEditor: 'code',
|
|
40
41
|
scan: await promptScanSettings(),
|
|
@@ -93,7 +94,8 @@ async function startSetup() {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
catch (error) {
|
|
96
|
-
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
console.error('\n❌ Setup failed:', message);
|
|
97
99
|
process.exit(1);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
@@ -155,7 +157,7 @@ function findProxyConfigFromAngularJson() {
|
|
|
155
157
|
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
|
156
158
|
const projects = angularJson?.projects ?? {};
|
|
157
159
|
for (const project of Object.values(projects)) {
|
|
158
|
-
const targets = project
|
|
160
|
+
const targets = project.architect || project.targets;
|
|
159
161
|
const serve = targets?.serve;
|
|
160
162
|
if (!serve)
|
|
161
163
|
continue;
|
|
@@ -193,54 +195,10 @@ function ensureGitignoreEntries(entries) {
|
|
|
193
195
|
fs.appendFileSync(gitignorePath, block);
|
|
194
196
|
console.log(`🧹 Added to .gitignore: ${missing.join(', ')}`);
|
|
195
197
|
}
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
201
|
-
return new Promise((resolve) => {
|
|
202
|
-
rl.question('🔌 Enter port number (press Enter for default: 4123): ', (answer) => {
|
|
203
|
-
rl.close();
|
|
204
|
-
const port = answer.trim();
|
|
205
|
-
const portNum = port === '' ? 4123 : parseInt(port, 10) || 4123;
|
|
206
|
-
console.log(` → Port: ${portNum}`);
|
|
207
|
-
resolve(portNum);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
function promptWorkspaceRoot() {
|
|
212
|
-
const rl = readline.createInterface({
|
|
213
|
-
input: process.stdin,
|
|
214
|
-
output: process.stdout,
|
|
215
|
-
});
|
|
216
|
-
console.log(`\n📁 Current directory: ${process.cwd()}`);
|
|
217
|
-
const askWorkspaceRoot = () => {
|
|
218
|
-
return new Promise((resolve) => {
|
|
219
|
-
rl.question('📁 Enter workspace root (press Enter for current directory "."): ', (answer) => {
|
|
220
|
-
const workspaceRoot = answer.trim();
|
|
221
|
-
const result = workspaceRoot === '' ? '.' : workspaceRoot;
|
|
222
|
-
const resolvedPath = path.resolve(process.cwd(), result);
|
|
223
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
224
|
-
console.log(` ❌ Path does not exist: ${resolvedPath}`);
|
|
225
|
-
console.log(' Please try again...\n');
|
|
226
|
-
askWorkspaceRoot().then(resolve);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const stat = fs.statSync(resolvedPath);
|
|
230
|
-
if (!stat.isDirectory()) {
|
|
231
|
-
console.log(` ❌ Path is not a directory: ${resolvedPath}`);
|
|
232
|
-
console.log(' Please try again...\n');
|
|
233
|
-
askWorkspaceRoot().then(resolve);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
console.log(` → Workspace root: ${result}`);
|
|
237
|
-
console.log(` → Resolved path: ${resolvedPath}`);
|
|
238
|
-
rl.close();
|
|
239
|
-
resolve(result);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
return askWorkspaceRoot();
|
|
198
|
+
function logDefaults() {
|
|
199
|
+
console.log('⚙️ Defaults applied:');
|
|
200
|
+
console.log(' → Port: 4123');
|
|
201
|
+
console.log(' → Workspace root: .');
|
|
244
202
|
}
|
|
245
203
|
function selectEditor() {
|
|
246
204
|
const availableEditors = [
|
|
@@ -296,7 +254,7 @@ function selectEditor() {
|
|
|
296
254
|
renderMenu();
|
|
297
255
|
break;
|
|
298
256
|
case '\r':
|
|
299
|
-
case '\n':
|
|
257
|
+
case '\n': {
|
|
300
258
|
process.stdin.setRawMode(false);
|
|
301
259
|
process.stdin.pause();
|
|
302
260
|
process.stdin.removeListener('data', handleKeypress);
|
|
@@ -304,6 +262,7 @@ function selectEditor() {
|
|
|
304
262
|
console.log(`\n✨ Selected: ${selected.name}`);
|
|
305
263
|
resolve(selected.value);
|
|
306
264
|
break;
|
|
265
|
+
}
|
|
307
266
|
case '\u0003':
|
|
308
267
|
console.log('\n\nCancelled. Setting Cursor as default.');
|
|
309
268
|
process.stdin.setRawMode(false);
|
package/dist/node/file-opener.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import express from 'express';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import childProcess from 'child_process';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
6
10
|
const root = process.cwd();
|
|
7
11
|
const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
|
|
8
12
|
const configPath = path.resolve(root, CONFIG_FILENAME);
|
|
@@ -12,8 +16,22 @@ if (!fs.existsSync(configPath)) {
|
|
|
12
16
|
console.log('Or manually create the config file.');
|
|
13
17
|
process.exit(1);
|
|
14
18
|
}
|
|
19
|
+
function isErrnoException(error) {
|
|
20
|
+
return typeof error === 'object' && error !== null && 'code' in error;
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_INCLUDE_GLOBS = [
|
|
23
|
+
'src/**/*.{ts,tsx}',
|
|
24
|
+
'projects/**/*.{ts,tsx}',
|
|
25
|
+
'apps/**/*.{ts,tsx}',
|
|
26
|
+
'libs/**/*.{ts,tsx}',
|
|
27
|
+
];
|
|
15
28
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
16
|
-
const
|
|
29
|
+
const WATCH_ENABLED = process.argv.includes('--watch') || process.argv.includes('-w');
|
|
30
|
+
const cfgScan = cfg.scan ?? {};
|
|
31
|
+
const scanIncludeGlobs = cfgScan.includeGlobs ?? DEFAULT_INCLUDE_GLOBS;
|
|
32
|
+
const scanWorkspaceRoot = cfg.workspaceRoot?.trim() || '.';
|
|
33
|
+
const cfgPort = cfg.port;
|
|
34
|
+
const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfgPort || 4123);
|
|
17
35
|
const MAP_PATH = path.resolve(root, '.open-in-editor/component-map.json');
|
|
18
36
|
const editorCLICache = {};
|
|
19
37
|
function checkEditorCLI(editorName, cliCommand = editorName) {
|
|
@@ -85,7 +103,8 @@ function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
|
|
|
85
103
|
return true;
|
|
86
104
|
}
|
|
87
105
|
catch (e) {
|
|
88
|
-
|
|
106
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
107
|
+
console.log(`[file-opener] EDITOR_CMD failed: ${message}`);
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
const tryRun = (editor) => {
|
|
@@ -98,7 +117,8 @@ function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
|
|
|
98
117
|
return true;
|
|
99
118
|
}
|
|
100
119
|
catch (e) {
|
|
101
|
-
|
|
120
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
121
|
+
console.log(`[file-opener] ${editor} failed: ${message}`);
|
|
102
122
|
return false;
|
|
103
123
|
}
|
|
104
124
|
};
|
|
@@ -108,13 +128,6 @@ function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
|
|
|
108
128
|
return true;
|
|
109
129
|
return false;
|
|
110
130
|
}
|
|
111
|
-
const app = express();
|
|
112
|
-
app.get('/__cmp-map', (req, res) => {
|
|
113
|
-
if (!fs.existsSync(MAP_PATH))
|
|
114
|
-
return res.status(404).send('component-map.json not found');
|
|
115
|
-
res.setHeader('Content-Type', 'application/json');
|
|
116
|
-
fs.createReadStream(MAP_PATH).pipe(res);
|
|
117
|
-
});
|
|
118
131
|
function findBestLineInFile(filePath, searchTerms) {
|
|
119
132
|
try {
|
|
120
133
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -147,49 +160,185 @@ function findBestLineInFile(filePath, searchTerms) {
|
|
|
147
160
|
return bestScore > 0 ? bestLine : 1;
|
|
148
161
|
}
|
|
149
162
|
catch (e) {
|
|
150
|
-
|
|
163
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
164
|
+
console.warn(`[file-opener] Failed to search in file: ${message}`);
|
|
151
165
|
return 1;
|
|
152
166
|
}
|
|
153
167
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
function startScanWatch() {
|
|
169
|
+
const scanScript = path.resolve(__dirname, 'cmp-scan.js');
|
|
170
|
+
if (!fs.existsSync(scanScript)) {
|
|
171
|
+
console.log('[file-opener] scan script not found, watch disabled.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const roots = getWatchRoots(scanIncludeGlobs, scanWorkspaceRoot);
|
|
175
|
+
if (roots.length === 0) {
|
|
176
|
+
console.log('[file-opener] watch roots not found, watch disabled.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const recursive = process.platform === 'darwin' || process.platform === 'win32';
|
|
180
|
+
const watchers = [];
|
|
181
|
+
let scanRunning = false;
|
|
182
|
+
let scanQueued = false;
|
|
183
|
+
let timer = null;
|
|
184
|
+
const runScan = (reason) => {
|
|
185
|
+
if (scanRunning) {
|
|
186
|
+
scanQueued = true;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
scanRunning = true;
|
|
190
|
+
const label = reason ? ` (${reason})` : '';
|
|
191
|
+
console.log(`[file-opener] scan started${label}`);
|
|
192
|
+
const scanProcess = spawn(process.execPath, [scanScript], {
|
|
193
|
+
stdio: 'inherit',
|
|
194
|
+
cwd: root,
|
|
195
|
+
});
|
|
196
|
+
scanProcess.on('close', (code) => {
|
|
197
|
+
scanRunning = false;
|
|
198
|
+
if (code === 0) {
|
|
199
|
+
console.log('[file-opener] scan completed');
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log('[file-opener] scan failed');
|
|
203
|
+
}
|
|
204
|
+
if (scanQueued) {
|
|
205
|
+
scanQueued = false;
|
|
206
|
+
scheduleScan('queued');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
const scheduleScan = (reason) => {
|
|
211
|
+
if (timer)
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
timer = setTimeout(() => runScan(reason), 500);
|
|
214
|
+
};
|
|
215
|
+
const attachWatcher = (watchPath) => {
|
|
216
|
+
try {
|
|
217
|
+
const watcher = fs.watch(watchPath, { recursive }, (eventType, filename) => {
|
|
218
|
+
const detail = filename ? `${eventType}:${filename.toString()}` : eventType;
|
|
219
|
+
scheduleScan(detail);
|
|
220
|
+
});
|
|
221
|
+
watchers.push(watcher);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.log(`[file-opener] failed to watch ${watchPath}: ${message}`);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
177
229
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
roots.forEach(attachWatcher);
|
|
231
|
+
console.log(`[file-opener] watch enabled (${recursive ? 'recursive' : 'non-recursive'}): ${roots.join(', ')}`);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
watchers.forEach((w) => w.close());
|
|
235
|
+
console.log('[file-opener] falling back to polling scan every 5s');
|
|
236
|
+
setInterval(() => runScan('poll'), 5000);
|
|
237
|
+
}
|
|
238
|
+
runScan('initial');
|
|
239
|
+
}
|
|
240
|
+
function getWatchRoots(includeGlobs, workspaceRoot) {
|
|
241
|
+
const roots = new Set();
|
|
242
|
+
includeGlobs.forEach((glob) => {
|
|
243
|
+
const base = globToBaseDir(glob);
|
|
244
|
+
const resolved = path.resolve(root, workspaceRoot, base);
|
|
245
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
246
|
+
roots.add(resolved);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return Array.from(roots);
|
|
250
|
+
}
|
|
251
|
+
function globToBaseDir(glob) {
|
|
252
|
+
const wildcardIndex = glob.search(/[*?[\]{]/);
|
|
253
|
+
if (wildcardIndex === -1)
|
|
254
|
+
return glob;
|
|
255
|
+
const prefix = glob.slice(0, wildcardIndex);
|
|
256
|
+
if (prefix.endsWith('/'))
|
|
257
|
+
return prefix.slice(0, -1);
|
|
258
|
+
return path.dirname(prefix);
|
|
259
|
+
}
|
|
260
|
+
const server = http.createServer((req, res) => {
|
|
261
|
+
if (!req.url) {
|
|
262
|
+
res.statusCode = 400;
|
|
263
|
+
res.end('Bad request');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (req.method !== 'GET') {
|
|
267
|
+
res.statusCode = 405;
|
|
268
|
+
res.end('Method not allowed');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
272
|
+
const pathname = url.pathname;
|
|
273
|
+
if (pathname === '/__cmp-map') {
|
|
274
|
+
if (!fs.existsSync(MAP_PATH)) {
|
|
275
|
+
res.statusCode = 404;
|
|
276
|
+
res.end('component-map.json not found');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
res.setHeader('Content-Type', 'application/json');
|
|
280
|
+
fs.createReadStream(MAP_PATH).pipe(res);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (pathname === '/__open-in-editor') {
|
|
284
|
+
const file = url.searchParams.get('file');
|
|
285
|
+
const line = url.searchParams.get('line') || '1';
|
|
286
|
+
const col = url.searchParams.get('col') || '1';
|
|
287
|
+
if (!file) {
|
|
288
|
+
res.statusCode = 400;
|
|
289
|
+
res.end('file is required');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const decoded = decodeURIComponent(file);
|
|
293
|
+
console.log(`[file-opener] Opening file: ${decoded}:${line}:${col}`);
|
|
294
|
+
const fileWithPos = `${decoded}:${line}:${col}`;
|
|
181
295
|
const ok = launchInEditor(fileWithPos);
|
|
182
296
|
if (!ok) {
|
|
183
|
-
|
|
297
|
+
res.statusCode = 500;
|
|
298
|
+
res.end('Failed to launch editor. Check PATH or set EDITOR_CMD.');
|
|
299
|
+
return;
|
|
184
300
|
}
|
|
185
|
-
res.end(
|
|
301
|
+
res.end('ok');
|
|
302
|
+
return;
|
|
186
303
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
304
|
+
if (pathname === '/__open-in-editor-search') {
|
|
305
|
+
const file = url.searchParams.get('file');
|
|
306
|
+
const searchParam = url.searchParams.get('search');
|
|
307
|
+
if (!file) {
|
|
308
|
+
res.statusCode = 400;
|
|
309
|
+
res.end('file is required');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!searchParam) {
|
|
313
|
+
res.statusCode = 400;
|
|
314
|
+
res.end('search terms required');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const decoded = decodeURIComponent(file);
|
|
318
|
+
try {
|
|
319
|
+
const searchTerms = JSON.parse(decodeURIComponent(searchParam));
|
|
320
|
+
const bestLine = findBestLineInFile(decoded, searchTerms);
|
|
321
|
+
const fileWithPos = `${decoded}:${bestLine}:1`;
|
|
322
|
+
const ok = launchInEditor(fileWithPos);
|
|
323
|
+
if (!ok) {
|
|
324
|
+
res.statusCode = 500;
|
|
325
|
+
res.end('Failed to launch editor');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
res.end(`Opened at line ${bestLine}`);
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
332
|
+
console.warn(`[file-opener] Search error: ${message}`);
|
|
333
|
+
res.statusCode = 500;
|
|
334
|
+
res.end('Search failed: ' + message);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
190
337
|
}
|
|
338
|
+
res.statusCode = 404;
|
|
339
|
+
res.end('Not found');
|
|
191
340
|
});
|
|
192
|
-
|
|
341
|
+
server
|
|
193
342
|
.listen(PORT, () => {
|
|
194
343
|
console.log(`[file-opener] http://localhost:${PORT}`);
|
|
195
344
|
console.log(` - map: ${path.relative(root, MAP_PATH)}`);
|
|
@@ -201,9 +350,12 @@ app
|
|
|
201
350
|
console.log(` • ${editor.name}${precision}`);
|
|
202
351
|
});
|
|
203
352
|
}
|
|
353
|
+
if (WATCH_ENABLED) {
|
|
354
|
+
startScanWatch();
|
|
355
|
+
}
|
|
204
356
|
})
|
|
205
357
|
.on('error', (err) => {
|
|
206
|
-
if (err.code === 'EADDRINUSE') {
|
|
358
|
+
if (isErrnoException(err) && err.code === 'EADDRINUSE') {
|
|
207
359
|
console.log(`[file-opener] Port ${PORT} already in use - another file:opener is already running`);
|
|
208
360
|
process.exit(0);
|
|
209
361
|
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-locatorjs",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "LocatorJs open-in-editor tools for Angular projects",
|
|
5
|
-
"author": "antepost24",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"license": "MIT",
|
|
8
5
|
"keywords": [
|
|
9
6
|
"angular",
|
|
10
|
-
"
|
|
11
|
-
"open-in-editor",
|
|
7
|
+
"component",
|
|
12
8
|
"devtools",
|
|
13
|
-
"
|
|
9
|
+
"locatorjs",
|
|
10
|
+
"open-in-editor"
|
|
14
11
|
],
|
|
12
|
+
"homepage": "https://github.com/Ea-st-ring/ngx-locator#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Ea-st-ring/ngx-locator/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "antepost24",
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
17
20
|
"url": "git+https://github.com/Ea-st-ring/ngx-locator.git"
|
|
18
21
|
},
|
|
19
|
-
"
|
|
20
|
-
"
|
|
22
|
+
"bin": {
|
|
23
|
+
"locatorjs-config": "dist/node/config-setup.js",
|
|
24
|
+
"locatorjs-open-in-editor": "dist/node/file-opener.js",
|
|
25
|
+
"locatorjs-scan": "dist/node/cmp-scan.js"
|
|
21
26
|
},
|
|
22
|
-
"
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
23
32
|
"main": "dist/browser/index.js",
|
|
24
33
|
"types": "dist/browser/index.d.ts",
|
|
25
34
|
"exports": {
|
|
@@ -34,37 +43,21 @@
|
|
|
34
43
|
"default": "./dist/browser/auto.js"
|
|
35
44
|
}
|
|
36
45
|
},
|
|
37
|
-
"bin": {
|
|
38
|
-
"locatorjs-open-in-editor": "dist/node/file-opener.js",
|
|
39
|
-
"locatorjs-config": "dist/node/config-setup.js",
|
|
40
|
-
"locatorjs-scan": "dist/node/cmp-scan.js"
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"dist",
|
|
44
|
-
"README.md"
|
|
45
|
-
],
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "npm run build:browser && npm run build:node",
|
|
48
48
|
"build:browser": "tsc -p tsconfig.browser.json",
|
|
49
49
|
"build:node": "tsc -p tsconfig.node.json",
|
|
50
50
|
"clean": "rm -rf dist",
|
|
51
51
|
"prepare": "npm run build",
|
|
52
|
-
"
|
|
53
|
-
"format": "
|
|
54
|
-
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
|
|
52
|
+
"format": "oxfmt",
|
|
53
|
+
"format:check": "oxfmt --check"
|
|
55
54
|
},
|
|
56
55
|
"dependencies": {
|
|
57
|
-
"
|
|
58
|
-
"ts-morph": "^27.0.0"
|
|
56
|
+
"glob": "^11.0.0"
|
|
59
57
|
},
|
|
60
58
|
"devDependencies": {
|
|
61
|
-
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
62
|
-
"@typescript-eslint/parser": "^7.18.0",
|
|
63
|
-
"@types/express": "^4.17.17",
|
|
64
59
|
"@types/node": "^18.18.0",
|
|
65
|
-
"
|
|
66
|
-
"eslint-config-prettier": "^9.1.0",
|
|
67
|
-
"prettier": "^3.2.5",
|
|
60
|
+
"oxfmt": "^0.35.0",
|
|
68
61
|
"typescript": "^5.5.4"
|
|
69
62
|
}
|
|
70
63
|
}
|