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 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~5 반드시 수행)**
13
+ **필수 단계 (1~4 반드시 수행)**
14
+
12
15
  1. 패키지 설치: `npm i -D ngx-locatorjs`
13
- 2. `main.ts`에 런타임 추가 (아래 예시 참고)
14
- 3. 설정/프록시 생성: `npx locatorjs-config`
15
- 4. 컴포넌트 스캔: `npx locatorjs-scan`
16
- 5. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
17
- - `npm run start` 사용 시 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
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
- - 동적/반복 템플릿의 정확한 라인 매칭은 100% 보장 불가
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
- - 프로젝트 루트에서 실행하고, `workspaceRoot` 질문에서 **Enter**를 누르면 `.`(현재 폴더)로 저장됩니다.
88
- - 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 그 **상대 경로**를 입력하세요. (예: `apps/web`)
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
- - `port`: file-opener 서버 포트
120
- - `workspaceRoot`: 실제 Angular 워크스페이스 루트(모노레포에서 하위 폴더일 때 사용)
121
- - `editor`: 기본 에디터 (`cursor`, `code`, `webstorm`)
122
- - `fallbackEditor`: 기본 에디터 실패 사용할 에디터
123
- - `scan.includeGlobs`: 컴포넌트 탐색 대상 경로
124
- - `scan.excludeGlobs`: 스캔 제외 경로
125
-
126
- **프로젝트 구조별 추천 includeGlobs**
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. component-map.json not found
170
- `npx locatorjs-scan` 실행 후 `.open-in-editor/component-map.json` 생성 여부 확인
171
- 4. 스캔 결과가 비어있거나 컴포넌트가 누락됨
172
- `scan.includeGlobs` 경로 확인 후 재스캔
173
- 5. 잘못된 파일이 열리거나 매칭이 안 됨
174
- `workspaceRoot`가 실제 Angular 루트인지 확인
175
- 6. 하이라이트가 보이거나 info가 null로 나옴
176
- `/__cmp-map` 응답에 컴포넌트 클래스명이 포함되는지 확인
177
- 7. 에디터가 열리지 않음
178
- CLI 설치 확인 또는 `EDITOR_CMD` 설정
179
- 8. 포트 충돌
180
- `ngx-locatorjs.config.json`과 `ngx-locatorjs.proxy.json`에서 포트 일치 여부 확인
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
+ ![2026-02-045 13 22-ezgif com-optimize](https://github.com/user-attachments/assets/9956e311-1af6-4096-9b9e-eb8dd6ea62be)
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
- You must complete steps 1–5 for this to work.
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. Add the runtime hook to `main.ts` (see the examples below)
26
- 3. Generate config + proxy: `npx locatorjs-config`
27
- 4. Scan components: `npx locatorjs-scan`
28
- 5. Run the file-opener server and your dev server (keep both running): `npx locatorjs-open-in-editor` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
29
- - If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
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((m) => m.installAngularLocator())
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((m) => m.installAngularLocator())
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
- - Run it from the project root and press **Enter** for `workspaceRoot: "."`.
81
- - In a monorepo, enter the **relative path** to your Angular app (e.g. `apps/web`).
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
- - `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
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 `/__cmp-map` is loading and includes your component class name
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
- - 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
234
+
207
235
  - Not supported in SSR/SSG runtime (browser DOM only)
@@ -1,2 +1,2 @@
1
1
  import { installAngularLocator } from './index';
2
- installAngularLocator();
2
+ installAngularLocator({ enableNetwork: true });
@@ -14,6 +14,7 @@ export type AngularLocatorEndpoints = {
14
14
  };
15
15
  export type AngularLocatorOptions = {
16
16
  endpoints?: Partial<AngularLocatorEndpoints>;
17
+ enableNetwork?: boolean;
17
18
  prefetchMap?: boolean;
18
19
  enableHover?: boolean;
19
20
  enableClick?: boolean;
@@ -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;AA+CF,iBAAe,SAAS,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB9D;AA2eD,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"}
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"}
@@ -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
  }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { Project, SyntaxKind } from 'ts-morph';
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(glob, workspaceRoot) {
35
+ function prefixWorkspaceRoot(globPattern, workspaceRoot) {
35
36
  if (!workspaceRoot || workspaceRoot === '.' || workspaceRoot === './')
36
- return glob;
37
- if (path.isAbsolute(glob))
38
- return glob;
37
+ return globPattern;
38
+ if (path.isAbsolute(globPattern))
39
+ return globPattern;
39
40
  const rootPosix = toPosix(workspaceRoot).replace(/\/+$/, '');
40
- const globPosix = toPosix(glob).replace(/^\/+/, '');
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 globToNeedle(glob) {
46
- return toPosix(glob).replace(/\*\*/g, '').replace(/\*/g, '');
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 isExcluded(filePath, excludeGlobs) {
49
- const normalized = toPosix(filePath);
50
- return excludeGlobs.some((pattern) => {
51
- const needle = globToNeedle(pattern);
52
- if (!needle)
53
- return false;
54
- return normalized.includes(needle);
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 project = new Project({
92
- skipAddingFilesFromTsConfig: true,
93
- });
94
- project.addSourceFilesAtPaths(effectiveIncludeGlobs);
95
- const sourceFiles = project
96
- .getSourceFiles()
97
- .filter((sf) => !isExcluded(sf.getFilePath(), excludeGlobs));
98
- const filePaths = sourceFiles.map((sf) => sf.getFilePath());
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 sf of sourceFiles) {
112
- const filePath = sf.getFilePath();
113
- const classes = sf.getClasses();
114
- for (const cls of classes) {
115
- const decorators = cls.getDecorators();
116
- const comp = decorators.find((d) => d.getName() === 'Component');
117
- if (!comp)
118
- continue;
119
- const arg = comp.getCallExpression()?.getArguments()[0];
120
- if (!arg || !arg.asKind(SyntaxKind.ObjectLiteralExpression))
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: await promptPort(),
37
- workspaceRoot: await promptWorkspaceRoot(),
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
- console.error('\n❌ Setup failed:', error?.message || error);
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?.architect || project?.targets;
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 promptPort() {
197
- const rl = readline.createInterface({
198
- input: process.stdin,
199
- output: process.stdout,
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);
@@ -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 PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfg.port || 4123);
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
- console.log(`[file-opener] EDITOR_CMD failed: ${e.message}`);
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
- console.log(`[file-opener] ${editor} failed: ${e.message}`);
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
- console.warn(`[file-opener] Failed to search in file: ${e.message}`);
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
- app.get('/__open-in-editor', (req, res) => {
155
- let file = req.query.file;
156
- const line = req.query.line || '1';
157
- const col = req.query.col || '1';
158
- if (!file)
159
- return res.status(400).send('file is required');
160
- file = decodeURIComponent(file);
161
- console.log(`[file-opener] Opening file: ${file}:${line}:${col}`);
162
- const fileWithPos = `${file}:${line}:${col}`;
163
- const ok = launchInEditor(fileWithPos);
164
- if (!ok) {
165
- return res.status(500).send('Failed to launch editor. Check PATH or set EDITOR_CMD.');
166
- }
167
- res.end('ok');
168
- });
169
- app.get('/__open-in-editor-search', (req, res) => {
170
- let file = req.query.file;
171
- const searchParam = req.query.search;
172
- if (!file)
173
- return res.status(400).send('file is required');
174
- if (!searchParam)
175
- return res.status(400).send('search terms required');
176
- file = decodeURIComponent(file);
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
- const searchTerms = JSON.parse(decodeURIComponent(searchParam));
179
- const bestLine = findBestLineInFile(file, searchTerms);
180
- const fileWithPos = `${file}:${bestLine}:1`;
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
- return res.status(500).send('Failed to launch editor');
297
+ res.statusCode = 500;
298
+ res.end('Failed to launch editor. Check PATH or set EDITOR_CMD.');
299
+ return;
184
300
  }
185
- res.end(`Opened at line ${bestLine}`);
301
+ res.end('ok');
302
+ return;
186
303
  }
187
- catch (e) {
188
- console.warn(`[file-opener] Search error: ${e.message}`);
189
- res.status(500).send('Search failed: ' + e.message);
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
- app
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.0",
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
- "locatorjs",
11
- "open-in-editor",
7
+ "component",
12
8
  "devtools",
13
- "component"
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
- "bugs": {
20
- "url": "https://github.com/Ea-st-ring/ngx-locator/issues"
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
- "homepage": "https://github.com/Ea-st-ring/ngx-locator#readme",
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
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
53
- "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
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
- "express": "^4.18.2",
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
- "eslint": "^8.57.1",
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
  }