ngx-locatorjs 0.1.0 → 0.2.0
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 +39 -34
- package/README.md +33 -28
- 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/config-setup.js +7 -50
- package/dist/node/file-opener.js +185 -41
- package/package.json +1 -3
package/README.ko.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
**기능**
|
|
6
7
|
- Alt+클릭: 템플릿(.html) 열기
|
|
@@ -8,13 +9,13 @@
|
|
|
8
9
|
- Alt 키 홀드: 컴포넌트 하이라이트 + 툴팁 표시
|
|
9
10
|
- Cursor, VS Code, WebStorm 지원
|
|
10
11
|
|
|
11
|
-
**필수 단계 (1~
|
|
12
|
+
**필수 단계 (1~4 반드시 수행)**
|
|
12
13
|
1. 패키지 설치: `npm i -D ngx-locatorjs`
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
2. 설정/프록시 생성: `npx locatorjs-config`
|
|
15
|
+
3. `main.ts`에 런타임 훅 추가 (아래 예시 참고)
|
|
16
|
+
4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
|
|
17
|
+
|
|
18
|
+
`npm run start` 사용 시 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
18
19
|
|
|
19
20
|
**Angular 코드 추가 (main.ts)**
|
|
20
21
|
```ts
|
|
@@ -32,7 +33,9 @@ platformBrowserDynamic()
|
|
|
32
33
|
.then(() => {
|
|
33
34
|
if (!environment.production) {
|
|
34
35
|
setTimeout(() => {
|
|
35
|
-
import('ngx-locatorjs').then((m) =>
|
|
36
|
+
import('ngx-locatorjs').then((m) =>
|
|
37
|
+
m.installAngularLocator({ enableNetwork: true }),
|
|
38
|
+
);
|
|
36
39
|
}, 1000);
|
|
37
40
|
}
|
|
38
41
|
})
|
|
@@ -49,7 +52,9 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
49
52
|
.then(() => {
|
|
50
53
|
setTimeout(() => {
|
|
51
54
|
import('ngx-locatorjs')
|
|
52
|
-
.then((m) =>
|
|
55
|
+
.then((m) =>
|
|
56
|
+
m.installAngularLocator({ enableNetwork: true }),
|
|
57
|
+
)
|
|
53
58
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
54
59
|
}, 1000);
|
|
55
60
|
})
|
|
@@ -76,7 +81,6 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
76
81
|
- 단일 Angular 앱, workspace, Nx 구조에서 동작
|
|
77
82
|
|
|
78
83
|
**불가능/제한 사항**
|
|
79
|
-
- 동적/반복 템플릿의 정확한 라인 매칭은 100% 보장 불가
|
|
80
84
|
- SSR/SSG 환경에서는 동작하지 않음 (브라우저 DOM 기반)
|
|
81
85
|
|
|
82
86
|
**ngx-locatorjs.config.json 가이드**
|
|
@@ -84,24 +88,30 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
84
88
|
|
|
85
89
|
**중요**
|
|
86
90
|
- `npx locatorjs-config`는 **실행한 현재 폴더를 기준**으로 설정합니다.
|
|
87
|
-
-
|
|
88
|
-
- 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 그 **상대
|
|
91
|
+
- 기본값: `port: 4123`, `workspaceRoot: "."`.
|
|
92
|
+
- 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 `workspaceRoot`를 그 **상대 경로**로 수정하세요. (예: `apps/web`)
|
|
89
93
|
- `.gitignore`가 있으면 `npx locatorjs-config`가 `.open-in-editor/`를 자동 추가합니다. 커밋하려면 해당 항목을 제거하세요.
|
|
90
94
|
|
|
91
95
|
예시:
|
|
92
96
|
```json
|
|
93
97
|
{
|
|
94
|
-
"port": 4123,
|
|
95
|
-
"workspaceRoot": ".",
|
|
96
|
-
"editor": "cursor",
|
|
97
|
-
"fallbackEditor": "code",
|
|
98
|
+
"port": 4123, // 프록시 서버가 실행될 포트 주소
|
|
99
|
+
"workspaceRoot": ".", // Angular 워크스페이스 루트
|
|
100
|
+
"editor": "cursor", // 파일을 열 때 사용할 에디터 (`cursor`, `code`, `webstorm`)
|
|
101
|
+
"fallbackEditor": "code", // 기본 에디터 실패 시 사용할 에디터
|
|
98
102
|
"scan": {
|
|
103
|
+
/**
|
|
104
|
+
* 탐색할 컴포넌트의 경로 목록
|
|
105
|
+
*/
|
|
99
106
|
"includeGlobs": [
|
|
100
107
|
"src/**/*.{ts,tsx}",
|
|
101
108
|
"projects/**/*.{ts,tsx}",
|
|
102
109
|
"apps/**/*.{ts,tsx}",
|
|
103
110
|
"libs/**/*.{ts,tsx}"
|
|
104
111
|
],
|
|
112
|
+
/**
|
|
113
|
+
* 스캔에서 제외할 경로 목록
|
|
114
|
+
*/
|
|
105
115
|
"excludeGlobs": [
|
|
106
116
|
"**/node_modules/**",
|
|
107
117
|
"**/dist/**",
|
|
@@ -115,15 +125,7 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
115
125
|
}
|
|
116
126
|
```
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
- `port`: file-opener 서버 포트
|
|
120
|
-
- `workspaceRoot`: 실제 Angular 워크스페이스 루트(모노레포에서 하위 폴더일 때 사용)
|
|
121
|
-
- `editor`: 기본 에디터 (`cursor`, `code`, `webstorm`)
|
|
122
|
-
- `fallbackEditor`: 기본 에디터 실패 시 사용할 에디터
|
|
123
|
-
- `scan.includeGlobs`: 컴포넌트 탐색 대상 경로
|
|
124
|
-
- `scan.excludeGlobs`: 스캔 제외 경로
|
|
125
|
-
|
|
126
|
-
**프로젝트 구조별 추천 includeGlobs**
|
|
128
|
+
**프로젝트 구조별 includeGlobs 예시**
|
|
127
129
|
1. 일반 Angular 앱
|
|
128
130
|
`["src/app/**/*.ts"]`
|
|
129
131
|
2. Angular Workspace (projects/)
|
|
@@ -166,21 +168,24 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
166
168
|
`ng serve --proxy-config ngx-locatorjs.proxy.json` 사용 여부 확인
|
|
167
169
|
2. npm run 경고
|
|
168
170
|
`npm run start -- --proxy-config ngx-locatorjs.proxy.json` 형태로 실행
|
|
169
|
-
3.
|
|
171
|
+
3. 네트워크 비활성
|
|
172
|
+
`installAngularLocator({ enableNetwork: true })` 설정 확인
|
|
173
|
+
4. component-map.json not found
|
|
170
174
|
`npx locatorjs-scan` 실행 후 `.open-in-editor/component-map.json` 생성 여부 확인
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
`
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
5. 컴포넌트 변경이 반영되지 않음
|
|
176
|
+
`npx locatorjs-open-in-editor --watch` 사용 또는 `npx locatorjs-scan` 재실행
|
|
177
|
+
6. 스캔 결과가 비어있거나 컴포넌트가 누락됨
|
|
178
|
+
`scan.includeGlobs` 경로 확인 후 재스캔. 실제 컴포넌트들이 위치한 경로를 입력해야 합니다.
|
|
179
|
+
7. 잘못된 파일이 열리거나 매칭이 안 됨
|
|
180
|
+
`workspaceRoot`가 Angular 앱 루트인지 확인
|
|
181
|
+
8. 하이라이트가 안 보이거나 info가 null로 나옴
|
|
182
|
+
`http://localhost:${port}/__cmp-map` 에서 컴포넌트 정보가 잘 나타나는지 확인
|
|
183
|
+
9. 포트 충돌
|
|
180
184
|
`ngx-locatorjs.config.json`과 `ngx-locatorjs.proxy.json`에서 포트 일치 여부 확인
|
|
181
185
|
|
|
182
186
|
**주의**
|
|
183
187
|
- 개발 모드에서만 사용하세요. 프로덕션 번들에 포함되지 않도록 `environment.production` 체크를 권장합니다.
|
|
188
|
+
- 네트워크 요청은 opt-in이며 localhost로만 제한됩니다. `enableNetwork: true`로 활성화하세요.
|
|
184
189
|
|
|
185
190
|
**원 커맨드 실행 (추천)**
|
|
186
191
|
file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식 중 하나를 사용하세요.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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:
|
|
@@ -7,6 +9,8 @@ Open Angular component files directly from the browser with **Alt + Click** duri
|
|
|
7
9
|
- CLI tools to scan Angular components and open files in your editor
|
|
8
10
|
- Config + proxy setup guidance
|
|
9
11
|
|
|
12
|
+
Inspired by [locatorjs.com](https://www.locatorjs.com/).
|
|
13
|
+
|
|
10
14
|
## Features
|
|
11
15
|
- **Alt + Click**: open template (.html)
|
|
12
16
|
- **Alt + Shift + Click**: open component (.ts)
|
|
@@ -19,14 +23,14 @@ npm i -D ngx-locatorjs
|
|
|
19
23
|
```
|
|
20
24
|
|
|
21
25
|
## Required Steps (Do This First)
|
|
22
|
-
You must complete steps 1–
|
|
26
|
+
You must complete steps 1–4 for this to work.
|
|
23
27
|
|
|
24
28
|
1. Install the package: `npm i -D ngx-locatorjs`
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
2. Generate config + proxy: `npx locatorjs-config`
|
|
30
|
+
3. Add the runtime hook to `main.ts` (see the examples below)
|
|
31
|
+
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`
|
|
32
|
+
|
|
33
|
+
If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
30
34
|
|
|
31
35
|
## Add to `main.ts`
|
|
32
36
|
|
|
@@ -47,7 +51,9 @@ platformBrowserDynamic()
|
|
|
47
51
|
if (!environment.production) {
|
|
48
52
|
setTimeout(() => {
|
|
49
53
|
import('ngx-locatorjs')
|
|
50
|
-
.then((m) =>
|
|
54
|
+
.then((m) =>
|
|
55
|
+
m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
|
|
56
|
+
)
|
|
51
57
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
52
58
|
}, 1000);
|
|
53
59
|
}
|
|
@@ -65,7 +71,9 @@ bootstrapApplication(AppComponent, appConfig)
|
|
|
65
71
|
.then(() => {
|
|
66
72
|
setTimeout(() => {
|
|
67
73
|
import('ngx-locatorjs')
|
|
68
|
-
.then((m) =>
|
|
74
|
+
.then((m) =>
|
|
75
|
+
m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
|
|
76
|
+
)
|
|
69
77
|
.catch((err) => console.warn('[angular-locator] Failed to load:', err));
|
|
70
78
|
}, 1000);
|
|
71
79
|
})
|
|
@@ -77,24 +85,30 @@ Location: project root
|
|
|
77
85
|
|
|
78
86
|
**Important**
|
|
79
87
|
- `npx locatorjs-config` uses the **current directory** as the base.
|
|
80
|
-
-
|
|
81
|
-
- In a monorepo,
|
|
88
|
+
- Defaults: `port: 4123`, `workspaceRoot: "."`.
|
|
89
|
+
- In a monorepo, update `workspaceRoot` to the **relative path** of your Angular app (e.g. `apps/web`).
|
|
82
90
|
- If `.gitignore` exists, `npx locatorjs-config` will append `.open-in-editor/`. Remove it if you want to commit the map.
|
|
83
91
|
|
|
84
92
|
Example:
|
|
85
93
|
```json
|
|
86
94
|
{
|
|
87
|
-
"port": 4123,
|
|
88
|
-
"workspaceRoot": ".",
|
|
89
|
-
"editor": "cursor",
|
|
90
|
-
"fallbackEditor": "code",
|
|
95
|
+
"port": 4123, // Port for the local file-opener server
|
|
96
|
+
"workspaceRoot": ".", // Angular workspace root
|
|
97
|
+
"editor": "cursor", // Editor to open files (`cursor`, `code`, `webstorm`)
|
|
98
|
+
"fallbackEditor": "code", // Fallback editor if the default fails
|
|
91
99
|
"scan": {
|
|
100
|
+
/**
|
|
101
|
+
* Globs to include when scanning components
|
|
102
|
+
*/
|
|
92
103
|
"includeGlobs": [
|
|
93
104
|
"src/**/*.{ts,tsx}",
|
|
94
105
|
"projects/**/*.{ts,tsx}",
|
|
95
106
|
"apps/**/*.{ts,tsx}",
|
|
96
107
|
"libs/**/*.{ts,tsx}"
|
|
97
108
|
],
|
|
109
|
+
/**
|
|
110
|
+
* Globs to exclude from scanning
|
|
111
|
+
*/
|
|
98
112
|
"excludeGlobs": [
|
|
99
113
|
"**/node_modules/**",
|
|
100
114
|
"**/dist/**",
|
|
@@ -108,15 +122,7 @@ Example:
|
|
|
108
122
|
}
|
|
109
123
|
```
|
|
110
124
|
|
|
111
|
-
###
|
|
112
|
-
- `port`: file-opener server port
|
|
113
|
-
- `workspaceRoot`: actual Angular workspace root
|
|
114
|
-
- `editor`: default editor (`cursor`, `code`, `webstorm`)
|
|
115
|
-
- `fallbackEditor`: used if default fails
|
|
116
|
-
- `scan.includeGlobs`: component scan targets
|
|
117
|
-
- `scan.excludeGlobs`: scan excludes
|
|
118
|
-
|
|
119
|
-
### Recommended includeGlobs
|
|
125
|
+
### Example includeGlobs
|
|
120
126
|
- Simple app: `"src/app/**/*.ts"`
|
|
121
127
|
- Angular workspace: `"projects/**/*.{ts,tsx}"`
|
|
122
128
|
- Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
|
|
@@ -154,15 +160,17 @@ Example:
|
|
|
154
160
|
## Troubleshooting
|
|
155
161
|
- **CORS / JSON parse error**: ensure dev server uses `--proxy-config ngx-locatorjs.proxy.json`
|
|
156
162
|
- **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
|
|
163
|
+
- **Network disabled**: pass `enableNetwork: true` to `installAngularLocator`
|
|
157
164
|
- **component-map.json not found**: run `npx locatorjs-scan`
|
|
165
|
+
- **Component changes not reflected**: run `npx locatorjs-open-in-editor --watch` or re-run `npx locatorjs-scan`
|
|
158
166
|
- **Map is empty or missing components**: check `scan.includeGlobs` and rerun the scan
|
|
159
167
|
- **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`
|
|
168
|
+
- **No highlight / info is null**: make sure `http://localhost:${port}/__cmp-map` is loading and includes your component class name
|
|
162
169
|
- **Port conflict**: change port in both `ngx-locatorjs.config.json` and `ngx-locatorjs.proxy.json`
|
|
163
170
|
|
|
164
171
|
## Notes
|
|
165
172
|
- Use only in development (guard with `environment.production`).
|
|
173
|
+
- Network requests are opt-in and limited to localhost. Set `enableNetwork: true` to activate.
|
|
166
174
|
|
|
167
175
|
## One-Command Dev (Recommended)
|
|
168
176
|
Running the file-opener server and Angular dev server separately is tedious. You can wire them into a single script.
|
|
@@ -201,7 +209,4 @@ npm i -D npm-run-all
|
|
|
201
209
|
- Works with single apps, Angular workspace, and Nx layouts
|
|
202
210
|
|
|
203
211
|
## Limitations
|
|
204
|
-
- Requires proxy setup (`ngx-locatorjs.proxy.json`), otherwise requests will fail
|
|
205
|
-
- Requires the file-opener server to be running
|
|
206
|
-
- Line matching in dynamic/repeated templates is heuristic, not perfect
|
|
207
212
|
- 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;AAEF,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,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;
|
|
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;AAEF,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,CAwB9D;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
|
}
|
|
@@ -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(),
|
|
@@ -193,54 +194,10 @@ function ensureGitignoreEntries(entries) {
|
|
|
193
194
|
fs.appendFileSync(gitignorePath, block);
|
|
194
195
|
console.log(`🧹 Added to .gitignore: ${missing.join(', ')}`);
|
|
195
196
|
}
|
|
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();
|
|
197
|
+
function logDefaults() {
|
|
198
|
+
console.log('⚙️ Defaults applied:');
|
|
199
|
+
console.log(' → Port: 4123');
|
|
200
|
+
console.log(' → Workspace root: .');
|
|
244
201
|
}
|
|
245
202
|
function selectEditor() {
|
|
246
203
|
const availableEditors = [
|
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,19 @@ if (!fs.existsSync(configPath)) {
|
|
|
12
16
|
console.log('Or manually create the config file.');
|
|
13
17
|
process.exit(1);
|
|
14
18
|
}
|
|
19
|
+
const DEFAULT_INCLUDE_GLOBS = [
|
|
20
|
+
'src/**/*.{ts,tsx}',
|
|
21
|
+
'projects/**/*.{ts,tsx}',
|
|
22
|
+
'apps/**/*.{ts,tsx}',
|
|
23
|
+
'libs/**/*.{ts,tsx}',
|
|
24
|
+
];
|
|
15
25
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
16
|
-
const
|
|
26
|
+
const WATCH_ENABLED = process.argv.includes('--watch') || process.argv.includes('-w');
|
|
27
|
+
const cfgScan = cfg.scan ?? {};
|
|
28
|
+
const scanIncludeGlobs = cfgScan.includeGlobs ?? DEFAULT_INCLUDE_GLOBS;
|
|
29
|
+
const scanWorkspaceRoot = cfg.workspaceRoot?.trim() || '.';
|
|
30
|
+
const cfgPort = cfg.port;
|
|
31
|
+
const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfgPort || 4123);
|
|
17
32
|
const MAP_PATH = path.resolve(root, '.open-in-editor/component-map.json');
|
|
18
33
|
const editorCLICache = {};
|
|
19
34
|
function checkEditorCLI(editorName, cliCommand = editorName) {
|
|
@@ -108,13 +123,6 @@ function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
|
|
|
108
123
|
return true;
|
|
109
124
|
return false;
|
|
110
125
|
}
|
|
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
126
|
function findBestLineInFile(filePath, searchTerms) {
|
|
119
127
|
try {
|
|
120
128
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -151,45 +159,178 @@ function findBestLineInFile(filePath, searchTerms) {
|
|
|
151
159
|
return 1;
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
162
|
+
function startScanWatch() {
|
|
163
|
+
const scanScript = path.resolve(__dirname, 'cmp-scan.js');
|
|
164
|
+
if (!fs.existsSync(scanScript)) {
|
|
165
|
+
console.log('[file-opener] scan script not found, watch disabled.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const roots = getWatchRoots(scanIncludeGlobs, scanWorkspaceRoot);
|
|
169
|
+
if (roots.length === 0) {
|
|
170
|
+
console.log('[file-opener] watch roots not found, watch disabled.');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const recursive = process.platform === 'darwin' || process.platform === 'win32';
|
|
174
|
+
const watchers = [];
|
|
175
|
+
let scanRunning = false;
|
|
176
|
+
let scanQueued = false;
|
|
177
|
+
let timer = null;
|
|
178
|
+
const runScan = (reason) => {
|
|
179
|
+
if (scanRunning) {
|
|
180
|
+
scanQueued = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
scanRunning = true;
|
|
184
|
+
const label = reason ? ` (${reason})` : '';
|
|
185
|
+
console.log(`[file-opener] scan started${label}`);
|
|
186
|
+
const scanProcess = spawn(process.execPath, [scanScript], {
|
|
187
|
+
stdio: 'inherit',
|
|
188
|
+
cwd: root,
|
|
189
|
+
});
|
|
190
|
+
scanProcess.on('close', (code) => {
|
|
191
|
+
scanRunning = false;
|
|
192
|
+
if (code === 0) {
|
|
193
|
+
console.log('[file-opener] scan completed');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log('[file-opener] scan failed');
|
|
197
|
+
}
|
|
198
|
+
if (scanQueued) {
|
|
199
|
+
scanQueued = false;
|
|
200
|
+
scheduleScan('queued');
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const scheduleScan = (reason) => {
|
|
205
|
+
if (timer)
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
timer = setTimeout(() => runScan(reason), 500);
|
|
208
|
+
};
|
|
209
|
+
const attachWatcher = (watchPath) => {
|
|
210
|
+
try {
|
|
211
|
+
const watcher = fs.watch(watchPath, { recursive }, (eventType, filename) => {
|
|
212
|
+
const detail = filename ? `${eventType}:${filename.toString()}` : eventType;
|
|
213
|
+
scheduleScan(detail);
|
|
214
|
+
});
|
|
215
|
+
watchers.push(watcher);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.log(`[file-opener] failed to watch ${watchPath}: ${err?.message || err}`);
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
177
222
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
223
|
+
roots.forEach(attachWatcher);
|
|
224
|
+
console.log(`[file-opener] watch enabled (${recursive ? 'recursive' : 'non-recursive'}): ${roots.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
watchers.forEach((w) => w.close());
|
|
228
|
+
console.log('[file-opener] falling back to polling scan every 5s');
|
|
229
|
+
setInterval(() => runScan('poll'), 5000);
|
|
230
|
+
}
|
|
231
|
+
runScan('initial');
|
|
232
|
+
}
|
|
233
|
+
function getWatchRoots(includeGlobs, workspaceRoot) {
|
|
234
|
+
const roots = new Set();
|
|
235
|
+
includeGlobs.forEach((glob) => {
|
|
236
|
+
const base = globToBaseDir(glob);
|
|
237
|
+
const resolved = path.resolve(root, workspaceRoot, base);
|
|
238
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
239
|
+
roots.add(resolved);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return Array.from(roots);
|
|
243
|
+
}
|
|
244
|
+
function globToBaseDir(glob) {
|
|
245
|
+
const wildcardIndex = glob.search(/[*?[\]{]/);
|
|
246
|
+
if (wildcardIndex === -1)
|
|
247
|
+
return glob;
|
|
248
|
+
const prefix = glob.slice(0, wildcardIndex);
|
|
249
|
+
if (prefix.endsWith('/'))
|
|
250
|
+
return prefix.slice(0, -1);
|
|
251
|
+
return path.dirname(prefix);
|
|
252
|
+
}
|
|
253
|
+
const server = http.createServer((req, res) => {
|
|
254
|
+
if (!req.url) {
|
|
255
|
+
res.statusCode = 400;
|
|
256
|
+
res.end('Bad request');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (req.method !== 'GET') {
|
|
260
|
+
res.statusCode = 405;
|
|
261
|
+
res.end('Method not allowed');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
265
|
+
const pathname = url.pathname;
|
|
266
|
+
if (pathname === '/__cmp-map') {
|
|
267
|
+
if (!fs.existsSync(MAP_PATH)) {
|
|
268
|
+
res.statusCode = 404;
|
|
269
|
+
res.end('component-map.json not found');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
res.setHeader('Content-Type', 'application/json');
|
|
273
|
+
fs.createReadStream(MAP_PATH).pipe(res);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (pathname === '/__open-in-editor') {
|
|
277
|
+
const file = url.searchParams.get('file');
|
|
278
|
+
const line = url.searchParams.get('line') || '1';
|
|
279
|
+
const col = url.searchParams.get('col') || '1';
|
|
280
|
+
if (!file) {
|
|
281
|
+
res.statusCode = 400;
|
|
282
|
+
res.end('file is required');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const decoded = decodeURIComponent(file);
|
|
286
|
+
console.log(`[file-opener] Opening file: ${decoded}:${line}:${col}`);
|
|
287
|
+
const fileWithPos = `${decoded}:${line}:${col}`;
|
|
181
288
|
const ok = launchInEditor(fileWithPos);
|
|
182
289
|
if (!ok) {
|
|
183
|
-
|
|
290
|
+
res.statusCode = 500;
|
|
291
|
+
res.end('Failed to launch editor. Check PATH or set EDITOR_CMD.');
|
|
292
|
+
return;
|
|
184
293
|
}
|
|
185
|
-
res.end(
|
|
294
|
+
res.end('ok');
|
|
295
|
+
return;
|
|
186
296
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
297
|
+
if (pathname === '/__open-in-editor-search') {
|
|
298
|
+
const file = url.searchParams.get('file');
|
|
299
|
+
const searchParam = url.searchParams.get('search');
|
|
300
|
+
if (!file) {
|
|
301
|
+
res.statusCode = 400;
|
|
302
|
+
res.end('file is required');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!searchParam) {
|
|
306
|
+
res.statusCode = 400;
|
|
307
|
+
res.end('search terms required');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const decoded = decodeURIComponent(file);
|
|
311
|
+
try {
|
|
312
|
+
const searchTerms = JSON.parse(decodeURIComponent(searchParam));
|
|
313
|
+
const bestLine = findBestLineInFile(decoded, searchTerms);
|
|
314
|
+
const fileWithPos = `${decoded}:${bestLine}:1`;
|
|
315
|
+
const ok = launchInEditor(fileWithPos);
|
|
316
|
+
if (!ok) {
|
|
317
|
+
res.statusCode = 500;
|
|
318
|
+
res.end('Failed to launch editor');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
res.end(`Opened at line ${bestLine}`);
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
console.warn(`[file-opener] Search error: ${e.message}`);
|
|
325
|
+
res.statusCode = 500;
|
|
326
|
+
res.end('Search failed: ' + e.message);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
190
329
|
}
|
|
330
|
+
res.statusCode = 404;
|
|
331
|
+
res.end('Not found');
|
|
191
332
|
});
|
|
192
|
-
|
|
333
|
+
server
|
|
193
334
|
.listen(PORT, () => {
|
|
194
335
|
console.log(`[file-opener] http://localhost:${PORT}`);
|
|
195
336
|
console.log(` - map: ${path.relative(root, MAP_PATH)}`);
|
|
@@ -201,6 +342,9 @@ app
|
|
|
201
342
|
console.log(` • ${editor.name}${precision}`);
|
|
202
343
|
});
|
|
203
344
|
}
|
|
345
|
+
if (WATCH_ENABLED) {
|
|
346
|
+
startScanWatch();
|
|
347
|
+
}
|
|
204
348
|
})
|
|
205
349
|
.on('error', (err) => {
|
|
206
350
|
if (err.code === 'EADDRINUSE') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngx-locatorjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "LocatorJs open-in-editor tools for Angular projects",
|
|
5
5
|
"author": "antepost24",
|
|
6
6
|
"type": "module",
|
|
@@ -54,13 +54,11 @@
|
|
|
54
54
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"express": "^4.18.2",
|
|
58
57
|
"ts-morph": "^27.0.0"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
61
60
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
62
61
|
"@typescript-eslint/parser": "^7.18.0",
|
|
63
|
-
"@types/express": "^4.17.17",
|
|
64
62
|
"@types/node": "^18.18.0",
|
|
65
63
|
"eslint": "^8.57.1",
|
|
66
64
|
"eslint-config-prettier": "^9.1.0",
|