ngx-locatorjs 0.2.1 → 0.4.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 CHANGED
@@ -8,16 +8,16 @@
8
8
  - Alt+클릭: 템플릿(.html) 열기
9
9
  - Alt+Shift+클릭: 컴포넌트(.ts) 열기
10
10
  - Alt 키 홀드: 컴포넌트 하이라이트 + 툴팁 표시
11
- - Cursor, VS Code, WebStorm 지원
11
+ - Antigravity IDE, Cursor, Zed, VS Code, WebStorm 지원
12
12
 
13
13
  **필수 단계 (1~4 반드시 수행)**
14
14
 
15
15
  1. 패키지 설치: `npm i -D ngx-locatorjs`
16
16
  2. 설정/프록시 생성: `npx locatorjs-config`
17
17
  3. `main.ts`에 런타임 훅 추가 (아래 예시 참고)
18
- 4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
18
+ 4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.cjs`
19
19
 
20
- `npm run start` 사용 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
20
+ 기존 0.3.0 버전에서 업그레이드했다면 `npx locatorjs-config`를 다시 실행해 `authToken`과 proxy 헤더를 재생성하고, `--proxy-config`(또는 `angular.json`)를 `ngx-locatorjs.proxy.cjs`로 맞추세요.
21
21
 
22
22
  **Angular 코드 추가 (main.ts)**
23
23
 
@@ -64,10 +64,10 @@ bootstrapApplication(AppComponent, appConfig)
64
64
  **Angular dev server 예시**
65
65
 
66
66
  - CLI 실행
67
- `ng serve --proxy-config ngx-locatorjs.proxy.json`
67
+ `ng serve --proxy-config ngx-locatorjs.proxy.cjs`
68
68
 
69
69
  - angular.json에 적용
70
- `"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.json"` 추가
70
+ `"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.cjs"` 추가
71
71
 
72
72
  **컴포넌트 맵 스캔**
73
73
 
@@ -93,7 +93,7 @@ bootstrapApplication(AppComponent, appConfig)
93
93
  **중요**
94
94
 
95
95
  - `npx locatorjs-config`는 **실행한 현재 폴더를 기준**으로 설정합니다.
96
- - 기본값: `port: 4123`, `workspaceRoot: "."`.
96
+ - 기본값: `host: "127.0.0.1"`, `port: 4123`, `workspaceRoot: "."`.
97
97
  - 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 `workspaceRoot`를 그 **상대 경로**로 수정하세요. (예: `apps/web`)
98
98
  - `.gitignore`가 있으면 `npx locatorjs-config`가 `.open-in-editor/`를 자동 추가합니다. 커밋하려면 해당 항목을 제거하세요.
99
99
 
@@ -101,10 +101,12 @@ bootstrapApplication(AppComponent, appConfig)
101
101
 
102
102
  ```json
103
103
  {
104
+ "host": "127.0.0.1",
104
105
  "port": 4123,
105
106
  "workspaceRoot": ".",
106
107
  "editor": "cursor",
107
108
  "fallbackEditor": "code",
109
+ "authToken": "locatorjs-config가 자동 생성",
108
110
  "scan": {
109
111
  "includeGlobs": [
110
112
  "src/**/*.{ts,tsx}",
@@ -128,9 +130,11 @@ bootstrapApplication(AppComponent, appConfig)
128
130
  **필드 설명**
129
131
 
130
132
  - `port`: 로컬 file-opener 서버 포트입니다.
133
+ - `host`: file-opener 서버 바인드 주소입니다. 로컬 전용으로 `127.0.0.1` 사용을 권장합니다.
131
134
  - `workspaceRoot`: 명령 실행 위치 기준 Angular 워크스페이스 루트 상대 경로입니다.
132
135
  - `editor`: 기본 에디터입니다 (`cursor`, `code`, `webstorm`).
133
136
  - `fallbackEditor`: 기본 에디터 실행 실패 시 사용할 대체 에디터입니다.
137
+ - `authToken`: open-in-editor 서버 요청 검증 토큰입니다. `locatorjs-config`가 자동 생성합니다.
134
138
  - `scan.includeGlobs`: 컴포넌트 소스 파일 탐색 대상 glob 목록입니다.
135
139
  - `scan.excludeGlobs`: 컴포넌트 스캔에서 제외할 glob 목록입니다.
136
140
 
@@ -150,37 +154,25 @@ bootstrapApplication(AppComponent, appConfig)
150
154
  3. `ngx-locatorjs.config.json`의 `editor`
151
155
  4. 자동 감지된 에디터
152
156
 
153
- **프록시 설정 (ngx-locatorjs.proxy.json)**
154
- `npx locatorjs-config` 실행 시 자동 생성됩니다. `angular.json`에 지정된 proxyConfig나 `proxy.conf.json`이 있으면 파일에 병합됩니다. 없으면 `ngx-locatorjs.proxy.json`을 생성합니다.
157
+ **프록시 설정 (ngx-locatorjs.proxy.cjs)**
158
+ `npx locatorjs-config` 실행 시 자동 생성됩니다. 파일은 실행 시점에 `ngx-locatorjs.config.json`을 읽어 target을 구성하므로, 포트를 바꾸려면 `ngx-locatorjs.config.json`의 `port`만 수정하면 됩니다.
155
159
 
156
160
  예시:
157
161
 
158
- ```json
159
- {
160
- "/__open-in-editor": {
161
- "target": "http://localhost:4123",
162
- "secure": false,
163
- "changeOrigin": true
164
- },
165
- "/__open-in-editor-search": {
166
- "target": "http://localhost:4123",
167
- "secure": false,
168
- "changeOrigin": true
169
- },
170
- "/__cmp-map": {
171
- "target": "http://localhost:4123",
172
- "secure": false,
173
- "changeOrigin": true
174
- }
175
- }
162
+ ```js
163
+ module.exports = {
164
+ '/__open-in-editor': { target, secure: false, changeOrigin: true, headers },
165
+ '/__open-in-editor-search': { target, secure: false, changeOrigin: true, headers },
166
+ '/__cmp-map': { target, secure: false, changeOrigin: true, headers },
167
+ };
176
168
  ```
177
169
 
178
170
  **트러블슈팅**
179
171
 
180
172
  1. CORS 에러
181
- `ng serve --proxy-config ngx-locatorjs.proxy.json` 사용 여부 확인
173
+ `ng serve --proxy-config ngx-locatorjs.proxy.cjs` 사용 여부 확인
182
174
  2. npm run 경고
183
- `npm run start -- --proxy-config ngx-locatorjs.proxy.json` 형태로 실행
175
+ `npm run start -- --proxy-config ngx-locatorjs.proxy.cjs` 형태로 실행
184
176
  3. 네트워크 비활성
185
177
  `installAngularLocator({ enableNetwork: true })` 설정 확인
186
178
  4. component-map.json not found
@@ -194,31 +186,20 @@ bootstrapApplication(AppComponent, appConfig)
194
186
  8. 하이라이트가 안 보이거나 info가 null로 나옴
195
187
  `http://localhost:${port}/__cmp-map` 에서 컴포넌트 정보가 잘 나타나는지 확인
196
188
  9. 포트 충돌
197
- `ngx-locatorjs.config.json`과 `ngx-locatorjs.proxy.json`에서 포트 일치 여부 확인
189
+ `ngx-locatorjs.config.json`의 `port`만 수정하면 됩니다
190
+ 10. opener 라우트에서 401 발생
191
+ `npx locatorjs-config` 재실행으로 토큰/프록시 헤더를 다시 생성하거나, 프록시 없이 직접 호출 시 `installAngularLocator`에 `authToken` 전달
198
192
 
199
193
  **주의**
200
194
 
201
195
  - 개발 모드에서만 사용하세요. 프로덕션 번들에 포함되지 않도록 `environment.production` 체크를 권장합니다.
202
196
  - 네트워크 요청은 opt-in이며 localhost로만 제한됩니다. `enableNetwork: true`로 활성화하세요.
197
+ - opener 서버는 기본적으로 loopback(`127.0.0.1`)에만 바인드되며, `workspaceRoot` 바깥 파일은 열 수 없습니다.
203
198
 
204
199
  **원 커맨드 실행 (추천)**
205
- file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식 중 하나를 사용하세요.
206
-
207
- ### Option A: `concurrently`
208
-
209
- ```bash
210
- npm i -D concurrently
211
- ```
212
-
213
- ```json
214
- {
215
- "scripts": {
216
- "dev:locator": "concurrently -k -n opener,ng \"npx locatorjs-open-in-editor\" \"ng serve --proxy-config ngx-locatorjs.proxy.json\""
217
- }
218
- }
219
- ```
200
+ file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식을 사용하세요.
220
201
 
221
- ### Option B: `npm-run-all`
202
+ ### `npm-run-all`
222
203
 
223
204
  ```bash
224
205
  npm i -D npm-run-all
@@ -228,8 +209,15 @@ npm i -D npm-run-all
228
209
  {
229
210
  "scripts": {
230
211
  "locator:opener": "npx locatorjs-open-in-editor",
231
- "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.json",
212
+ "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.cjs",
232
213
  "dev:locator": "run-p locator:opener dev:app"
233
214
  }
234
215
  }
235
216
  ```
217
+
218
+ ** 고민 **
219
+
220
+ 처음 세팅할 때 귀찮은 단계가 많다고 느낍니다.
221
+ 설치하고, `npx locatorjs-config`를 돌리고, `main.ts`에 훅을 넣고, opener와 dev server를 proxy 설정과 함께 실행..
222
+
223
+ 이 흐름을 더 짧고 자연스럽게 만들고 싶습니다. 의견은 언제나 환영합니다.
package/README.md CHANGED
@@ -17,7 +17,7 @@ Inspired by [locatorjs.com](https://www.locatorjs.com/).
17
17
  - **Alt + Click**: open template (.html)
18
18
  - **Alt + Shift + Click**: open component (.ts)
19
19
  - **Hold Alt**: highlight component + tooltip
20
- - Supports **Cursor**, **VS Code**, **WebStorm**
20
+ - Supports **Antigravity IDE**, **Cursor**, **Zed**, **VS Code**, **WebStorm**
21
21
 
22
22
  ## Install
23
23
 
@@ -32,9 +32,9 @@ You must complete steps 1–4 for this to work.
32
32
  1. Install the package: `npm i -D ngx-locatorjs`
33
33
  2. Generate config + proxy: `npx locatorjs-config`
34
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`
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.cjs`
36
36
 
37
- If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
37
+ If you are upgrading from an older version, run `npx locatorjs-config` again so `authToken` and proxy headers are generated, then use `ngx-locatorjs.proxy.cjs` in your `--proxy-config` or `angular.json`.
38
38
 
39
39
  ## Add to `main.ts`
40
40
 
@@ -93,7 +93,7 @@ Location: project root
93
93
  **Important**
94
94
 
95
95
  - `npx locatorjs-config` uses the **current directory** as the base.
96
- - Defaults: `port: 4123`, `workspaceRoot: "."`.
96
+ - Defaults: `host: "127.0.0.1"`, `port: 4123`, `workspaceRoot: "."`.
97
97
  - In a monorepo, update `workspaceRoot` to the **relative path** of your Angular app (e.g. `apps/web`).
98
98
  - If `.gitignore` exists, `npx locatorjs-config` will append `.open-in-editor/`. Remove it if you want to commit the map.
99
99
 
@@ -101,10 +101,12 @@ Example:
101
101
 
102
102
  ```json
103
103
  {
104
+ "host": "127.0.0.1",
104
105
  "port": 4123,
105
106
  "workspaceRoot": ".",
106
107
  "editor": "cursor",
107
108
  "fallbackEditor": "code",
109
+ "authToken": "generated-by-locatorjs-config",
108
110
  "scan": {
109
111
  "includeGlobs": [
110
112
  "src/**/*.{ts,tsx}",
@@ -128,9 +130,11 @@ Example:
128
130
  ### Field Reference
129
131
 
130
132
  - `port`: Port for the local file-opener server.
133
+ - `host`: Bind address for the file-opener server. Keep `127.0.0.1` for local-only access.
131
134
  - `workspaceRoot`: Angular workspace root path (relative to where you run commands).
132
135
  - `editor`: Preferred editor (`cursor`, `code`, `webstorm`).
133
136
  - `fallbackEditor`: Fallback editor if the preferred editor cannot be launched.
137
+ - `authToken`: Request token validated by the open-in-editor server. Generated automatically by `locatorjs-config`.
134
138
  - `scan.includeGlobs`: Globs used to find component source files.
135
139
  - `scan.excludeGlobs`: Globs excluded from component scanning.
136
140
 
@@ -140,30 +144,18 @@ Example:
140
144
  - Angular workspace: `"projects/**/*.{ts,tsx}"`
141
145
  - Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
142
146
 
143
- ## Proxy (`ngx-locatorjs.proxy.json`)
147
+ ## Proxy (`ngx-locatorjs.proxy.cjs`)
144
148
 
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`.
149
+ Generated by `npx locatorjs-config`. This dynamic proxy module reads `ngx-locatorjs.config.json` at runtime, so changing `port` in config updates proxy target automatically.
146
150
 
147
151
  Example:
148
152
 
149
- ```json
150
- {
151
- "/__open-in-editor": {
152
- "target": "http://localhost:4123",
153
- "secure": false,
154
- "changeOrigin": true
155
- },
156
- "/__open-in-editor-search": {
157
- "target": "http://localhost:4123",
158
- "secure": false,
159
- "changeOrigin": true
160
- },
161
- "/__cmp-map": {
162
- "target": "http://localhost:4123",
163
- "secure": false,
164
- "changeOrigin": true
165
- }
166
- }
153
+ ```js
154
+ module.exports = {
155
+ '/__open-in-editor': { target, secure: false, changeOrigin: true, headers },
156
+ '/__open-in-editor-search': { target, secure: false, changeOrigin: true, headers },
157
+ '/__cmp-map': { target, secure: false, changeOrigin: true, headers },
158
+ };
167
159
  ```
168
160
 
169
161
  ## Environment Variable Priority
@@ -175,40 +167,28 @@ Example:
175
167
 
176
168
  ## Troubleshooting
177
169
 
178
- - **CORS / JSON parse error**: ensure dev server uses `--proxy-config ngx-locatorjs.proxy.json`
179
- - **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
170
+ - **CORS / JSON parse error**: ensure dev server uses `--proxy-config ngx-locatorjs.proxy.cjs`
171
+ - **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.cjs`
180
172
  - **Network disabled**: pass `enableNetwork: true` to `installAngularLocator`
181
173
  - **component-map.json not found**: run `npx locatorjs-scan`
182
174
  - **Component changes not reflected**: run `npx locatorjs-open-in-editor --watch` or re-run `npx locatorjs-scan`
183
175
  - **Map is empty or missing components**: check `scan.includeGlobs` and rerun the scan
184
176
  - **Wrong files open or nothing matches**: confirm `workspaceRoot` points to the actual Angular app root
185
177
  - **No highlight / info is null**: make sure `http://localhost:${port}/__cmp-map` is loading and includes your component class name
186
- - **Port conflict**: change port in both `ngx-locatorjs.config.json` and `ngx-locatorjs.proxy.json`
178
+ - **Port conflict**: change `port` in `ngx-locatorjs.config.json` only
179
+ - **401 Unauthorized from opener routes**: regenerate config/proxy (`npx locatorjs-config`) or pass `authToken` to `installAngularLocator` if you call the opener without Angular proxy headers
187
180
 
188
181
  ## Notes
189
182
 
190
183
  - Use only in development (guard with `environment.production`).
191
184
  - Network requests are opt-in and limited to localhost. Set `enableNetwork: true` to activate.
185
+ - Opener server accepts files only inside `workspaceRoot` and binds to loopback by default.
192
186
 
193
187
  ## One-Command Dev (Recommended)
194
188
 
195
189
  Running the file-opener server and Angular dev server separately is tedious. You can wire them into a single script.
196
190
 
197
- ### Option A: `concurrently`
198
-
199
- ```bash
200
- npm i -D concurrently
201
- ```
202
-
203
- ```json
204
- {
205
- "scripts": {
206
- "dev:locator": "concurrently -k -n opener,ng \"npx locatorjs-open-in-editor\" \"ng serve --proxy-config ngx-locatorjs.proxy.json\""
207
- }
208
- }
209
- ```
210
-
211
- ### Option B: `npm-run-all`
191
+ ### `npm-run-all`
212
192
 
213
193
  ```bash
214
194
  npm i -D npm-run-all
@@ -218,7 +198,7 @@ npm i -D npm-run-all
218
198
  {
219
199
  "scripts": {
220
200
  "locator:opener": "npx locatorjs-open-in-editor",
221
- "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.json",
201
+ "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.cjs",
222
202
  "dev:locator": "run-p locator:opener dev:app"
223
203
  }
224
204
  }
@@ -233,3 +213,12 @@ npm i -D npm-run-all
233
213
  ## Limitations
234
214
 
235
215
  - Not supported in SSR/SSG runtime (browser DOM only)
216
+
217
+ ## Maintainer Note (Setup UX)
218
+
219
+ Current setup works, but the first-time flow still feels too manual. You install, run
220
+ `npx locatorjs-config`, add the bootstrap hook in `main.ts`, and then run opener + dev server
221
+ with proxy config.
222
+
223
+ This is an area we want to keep improving. Contributions that reduce setup friction are welcome,
224
+ especially around safer automation, clearer defaults, and better error guidance.
@@ -20,6 +20,7 @@ export type AngularLocatorOptions = {
20
20
  enableClick?: boolean;
21
21
  showTooltip?: boolean;
22
22
  showClickFeedback?: boolean;
23
+ authToken?: string;
23
24
  debug?: boolean;
24
25
  };
25
26
  declare function ensureMap(forceRefresh?: boolean): Promise<CmpMap>;
@@ -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;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"}
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,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AA+FF,iBAAe,SAAS,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CA4B9D;AAmfD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,iBAc9E;AAED,wBAAsB,mBAAmB,kBASxC;AAED,wBAAsB,mBAAmB,kBAExC;AAED,wBAAgB,yBAAyB,YAExC;AAED,eAAO,MAAM,SAAS;;CAErB,CAAC"}
@@ -11,6 +11,7 @@ const DEFAULT_OPTIONS = {
11
11
  enableClick: true,
12
12
  showTooltip: true,
13
13
  showClickFeedback: true,
14
+ authToken: null,
14
15
  debug: false,
15
16
  };
16
17
  let OPTIONS = DEFAULT_OPTIONS;
@@ -41,6 +42,22 @@ function assertNetworkAllowed(url) {
41
42
  }
42
43
  return resolved.toString();
43
44
  }
45
+ function attachAuthToken(url) {
46
+ if (!OPTIONS.authToken)
47
+ return url;
48
+ const resolved = new URL(url, window.location.href);
49
+ if (!resolved.searchParams.has('token')) {
50
+ resolved.searchParams.set('token', OPTIONS.authToken);
51
+ }
52
+ return resolved.toString();
53
+ }
54
+ function getAuthHeaders() {
55
+ if (!OPTIONS.authToken)
56
+ return undefined;
57
+ return {
58
+ 'x-locatorjs-token': OPTIONS.authToken,
59
+ };
60
+ }
44
61
  function normalizeMap(map) {
45
62
  if (!map.filePathsByClassName || Object.keys(map.filePathsByClassName).length === 0) {
46
63
  const rebuilt = {};
@@ -59,12 +76,13 @@ async function ensureMap(forceRefresh = false) {
59
76
  if (CMP_MAP && !forceRefresh)
60
77
  return CMP_MAP;
61
78
  const timestamp = Date.now();
62
- const res = await fetch(assertNetworkAllowed(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`), {
79
+ const res = await fetch(attachAuthToken(assertNetworkAllowed(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`)), {
63
80
  cache: 'no-store',
64
81
  headers: {
65
82
  'Cache-Control': 'no-cache, no-store, must-revalidate',
66
83
  Pragma: 'no-cache',
67
84
  Expires: '0',
85
+ ...getAuthHeaders(),
68
86
  },
69
87
  });
70
88
  const text = await res.text();
@@ -198,9 +216,9 @@ function getNearestComponent(el) {
198
216
  };
199
217
  }
200
218
  async function openFile(absPath, line = 1, col = 1) {
201
- const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`);
219
+ const url = attachAuthToken(assertNetworkAllowed(`${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`));
202
220
  try {
203
- await fetch(url);
221
+ await fetch(url, { headers: getAuthHeaders() });
204
222
  }
205
223
  catch (e) {
206
224
  if (OPTIONS.debug) {
@@ -209,9 +227,9 @@ async function openFile(absPath, line = 1, col = 1) {
209
227
  }
210
228
  }
211
229
  async function openFileWithSearch(absPath, searchTerms) {
212
- const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`);
230
+ const url = attachAuthToken(assertNetworkAllowed(`${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`));
213
231
  try {
214
- await fetch(url);
232
+ await fetch(url, { headers: getAuthHeaders() });
215
233
  }
216
234
  catch (e) {
217
235
  if (OPTIONS.debug) {
@@ -524,6 +542,7 @@ export async function installAngularLocator(options = {}) {
524
542
  const mergedOptions = {
525
543
  ...DEFAULT_OPTIONS,
526
544
  ...options,
545
+ authToken: options.authToken ?? DEFAULT_OPTIONS.authToken,
527
546
  endpoints: {
528
547
  ...DEFAULT_ENDPOINTS,
529
548
  ...options.endpoints,
@@ -110,10 +110,31 @@ async function main() {
110
110
  fs.mkdirSync(outDir, { recursive: true });
111
111
  function loadCache() {
112
112
  try {
113
- return JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
113
+ const raw = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
114
+ if (!raw || typeof raw !== 'object')
115
+ return { version: 2, files: {} };
116
+ const typed = raw;
117
+ if (typed.version === 2 && typed.files && typeof typed.files === 'object') {
118
+ return {
119
+ version: 2,
120
+ files: typed.files,
121
+ };
122
+ }
123
+ // Backward compatibility for old cache shape: { [filePath]: mtimeMs }
124
+ const files = {};
125
+ for (const [filePath, mtime] of Object.entries(raw)) {
126
+ if (typeof mtime === 'number') {
127
+ files[filePath] = {
128
+ // Force one-time reparsing after legacy-cache migration.
129
+ mtimeMs: -1,
130
+ components: [],
131
+ };
132
+ }
133
+ }
134
+ return { version: 2, files };
114
135
  }
115
136
  catch {
116
- return {};
137
+ return { version: 2, files: {} };
117
138
  }
118
139
  }
119
140
  function saveCache(cache) {
@@ -144,19 +165,36 @@ async function main() {
144
165
  const filePaths = [...new Set(allFiles)];
145
166
  const currentStats = getFileStats(filePaths);
146
167
  const previousCache = loadCache();
147
- const hasChanges = filePaths.some((filePath) => !previousCache[filePath] || previousCache[filePath] !== currentStats[filePath]);
148
- const cachedPaths = Object.keys(previousCache);
149
- const hasNewOrDeletedFiles = filePaths.length !== cachedPaths.length ||
150
- filePaths.some((p) => !previousCache[p]) ||
151
- cachedPaths.some((p) => !currentStats[p]);
152
- if (!hasChanges && !hasNewOrDeletedFiles && fs.existsSync(outFile)) {
168
+ const previousFiles = previousCache.files;
169
+ const previousPaths = Object.keys(previousFiles);
170
+ const changedOrNewPaths = filePaths.filter((filePath) => {
171
+ const currentMtime = currentStats[filePath];
172
+ const cached = previousFiles[filePath];
173
+ return !cached || cached.mtimeMs !== currentMtime;
174
+ });
175
+ const deletedPaths = previousPaths.filter((cachedPath) => !currentStats[cachedPath]);
176
+ if (changedOrNewPaths.length === 0 && deletedPaths.length === 0 && fs.existsSync(outFile)) {
153
177
  process.exit(0);
154
178
  }
155
- const detailByFilePath = {};
156
- const filePathsByClassName = {};
179
+ const nextFiles = {};
157
180
  for (const filePath of filePaths) {
181
+ const previous = previousFiles[filePath];
182
+ const currentMtime = currentStats[filePath];
183
+ if (previous && previous.mtimeMs === currentMtime) {
184
+ nextFiles[filePath] = previous;
185
+ continue;
186
+ }
158
187
  const sourceCode = fs.readFileSync(filePath, 'utf8');
159
188
  const components = parseSourceFile(filePath, sourceCode);
189
+ nextFiles[filePath] = {
190
+ mtimeMs: currentMtime,
191
+ components,
192
+ };
193
+ }
194
+ const detailByFilePath = {};
195
+ const filePathsByClassName = {};
196
+ for (const entry of Object.values(nextFiles)) {
197
+ const components = entry.components;
160
198
  for (const cmp of components) {
161
199
  detailByFilePath[cmp.filePath] = cmp;
162
200
  if (!filePathsByClassName[cmp.className]) {
@@ -173,8 +211,11 @@ async function main() {
173
211
  filePathsByClassName,
174
212
  };
175
213
  fs.writeFileSync(outFile, JSON.stringify(out, null, 2));
176
- saveCache(currentStats);
177
- console.log(`[cmp-scan] ✅ Saved ${Object.keys(detailByFilePath).length} components to ${path.relative(root, outFile)}`);
214
+ saveCache({
215
+ version: 2,
216
+ files: nextFiles,
217
+ });
218
+ console.log(`[cmp-scan] ✅ Saved ${Object.keys(detailByFilePath).length} components to ${path.relative(root, outFile)} (updated ${changedOrNewPaths.length}, removed ${deletedPaths.length})`);
178
219
  }
179
220
  main().catch((err) => {
180
221
  console.error('[cmp-scan] Failed:', err);
@@ -3,12 +3,13 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import readline from 'readline';
5
5
  import { spawn } from 'child_process';
6
+ import crypto from 'crypto';
6
7
  import { fileURLToPath } from 'url';
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const root = process.cwd();
10
11
  const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
11
- const PROXY_FILENAME = 'ngx-locatorjs.proxy.json';
12
+ const PROXY_FILENAME = 'ngx-locatorjs.proxy.cjs';
12
13
  const configPath = path.resolve(root, CONFIG_FILENAME);
13
14
  const proxyConfigPath = resolveProxyConfigPath();
14
15
  console.log('🚀 LocatorJs (Open-in-Editor) Configuration Setup\n');
@@ -33,36 +34,22 @@ else {
33
34
  async function startSetup() {
34
35
  try {
35
36
  logDefaults();
37
+ const authToken = generateAuthToken();
36
38
  const config = {
37
39
  port: 4123,
40
+ host: '127.0.0.1',
38
41
  workspaceRoot: '.',
39
42
  editor: await selectEditor(),
40
43
  fallbackEditor: 'code',
44
+ authToken,
41
45
  scan: await promptScanSettings(),
42
46
  };
43
47
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
44
- const proxyConfig = {
45
- '/__open-in-editor': {
46
- target: `http://localhost:${config.port}`,
47
- secure: false,
48
- changeOrigin: true,
49
- },
50
- '/__open-in-editor-search': {
51
- target: `http://localhost:${config.port}`,
52
- secure: false,
53
- changeOrigin: true,
54
- },
55
- '/__cmp-map': {
56
- target: `http://localhost:${config.port}`,
57
- secure: false,
58
- changeOrigin: true,
59
- },
60
- };
61
- const mergedProxyConfig = mergeProxyConfig(proxyConfigPath, proxyConfig);
62
- fs.writeFileSync(proxyConfigPath, JSON.stringify(mergedProxyConfig, null, 2));
48
+ fs.writeFileSync(proxyConfigPath, buildDynamicProxyModule());
63
49
  console.log('\n✅ Configuration saved successfully!');
64
50
  console.log(`📁 Config: ${path.relative(root, configPath)}`);
65
- console.log(`🔗 Proxy: ${path.relative(root, proxyConfigPath)} (port: ${config.port})`);
51
+ console.log(`🔗 Proxy: ${path.relative(root, proxyConfigPath)} (dynamic from config port)`);
52
+ console.log('🔒 Request auth token generated and wired into proxy headers');
66
53
  ensureGitignoreEntries(['.open-in-editor/']);
67
54
  console.log('\n🔍 Running component scan...');
68
55
  const scanScript = path.resolve(__dirname, 'cmp-scan.js');
@@ -108,45 +95,65 @@ function printNextSteps(proxyPath) {
108
95
  console.log(' npx locatorjs-open-in-editor');
109
96
  console.log(` (run your Angular dev server with --proxy-config ${path.relative(root, proxyPath)})`);
110
97
  }
111
- function mergeProxyConfig(proxyConfigPath, addition) {
112
- const existing = readProxyConfig(proxyConfigPath);
113
- if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
114
- return {
115
- ...existing,
116
- ...addition,
117
- };
118
- }
119
- if (fs.existsSync(proxyConfigPath)) {
120
- console.log('⚠️ Existing proxy config is not valid JSON. Overwriting with locator config.');
121
- }
122
- return addition;
98
+ function buildDynamicProxyModule() {
99
+ return `'use strict';
100
+
101
+ const fs = require('fs');
102
+ const path = require('path');
103
+
104
+ function loadLocatorConfig() {
105
+ const configPath = path.resolve(process.cwd(), '${CONFIG_FILENAME}');
106
+ try {
107
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
108
+ } catch {
109
+ return {};
110
+ }
123
111
  }
124
- function readProxyConfig(filePath) {
125
- if (!fs.existsSync(filePath))
126
- return null;
127
- try {
128
- const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
129
- if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
130
- return existing;
131
- }
132
- }
133
- catch {
134
- // ignore parse errors
135
- }
136
- return null;
112
+
113
+ const cfg = loadLocatorConfig();
114
+ const host = typeof cfg.host === 'string' && cfg.host.trim() ? cfg.host.trim() : '127.0.0.1';
115
+ const port = Number(process.env.OPEN_IN_EDITOR_PORT || cfg.port || 4123);
116
+ const authToken = typeof cfg.authToken === 'string' ? cfg.authToken : '';
117
+ const headers = authToken ? { 'x-locatorjs-token': authToken } : {};
118
+ const target = \`http://\${host}:\${port}\`;
119
+
120
+ module.exports = {
121
+ '/__open-in-editor': {
122
+ target,
123
+ secure: false,
124
+ changeOrigin: true,
125
+ headers,
126
+ },
127
+ '/__open-in-editor-search': {
128
+ target,
129
+ secure: false,
130
+ changeOrigin: true,
131
+ headers,
132
+ },
133
+ '/__cmp-map': {
134
+ target,
135
+ secure: false,
136
+ changeOrigin: true,
137
+ headers,
138
+ },
139
+ };
140
+ `;
137
141
  }
138
142
  function resolveProxyConfigPath() {
139
143
  const angularProxyPath = findProxyConfigFromAngularJson();
140
144
  if (angularProxyPath) {
141
- if (path.extname(angularProxyPath) !== '.json') {
142
- console.log(`⚠️ proxyConfig in angular.json is not a JSON file (${path.basename(angularProxyPath)}). Creating ${PROXY_FILENAME} instead.`);
145
+ const ext = path.extname(angularProxyPath).toLowerCase();
146
+ if (ext === '.js' || ext === '.cjs') {
147
+ return angularProxyPath;
148
+ }
149
+ if (ext === '.json') {
150
+ console.log(`⚠️ angular.json points to JSON proxy (${path.basename(angularProxyPath)}).`);
151
+ console.log(` Creating dynamic proxy module ${PROXY_FILENAME} instead. Update angular.json proxyConfig to use it for single-source port management.`);
143
152
  return path.resolve(root, PROXY_FILENAME);
144
153
  }
145
- return angularProxyPath;
154
+ console.log(`⚠️ Unsupported proxy extension (${path.basename(angularProxyPath)}). Creating ${PROXY_FILENAME} instead.`);
155
+ return path.resolve(root, PROXY_FILENAME);
146
156
  }
147
- const defaultProxy = path.resolve(root, 'proxy.conf.json');
148
- if (fs.existsSync(defaultProxy))
149
- return defaultProxy;
150
157
  return path.resolve(root, PROXY_FILENAME);
151
158
  }
152
159
  function findProxyConfigFromAngularJson() {
@@ -198,11 +205,17 @@ function ensureGitignoreEntries(entries) {
198
205
  function logDefaults() {
199
206
  console.log('⚙️ Defaults applied:');
200
207
  console.log(' → Port: 4123');
208
+ console.log(' → Host: 127.0.0.1');
201
209
  console.log(' → Workspace root: .');
202
210
  }
211
+ function generateAuthToken() {
212
+ return crypto.randomBytes(24).toString('hex');
213
+ }
203
214
  function selectEditor() {
204
215
  const availableEditors = [
205
216
  { name: 'Cursor', value: 'cursor' },
217
+ { name: 'Zed', value: 'zed' },
218
+ { name: 'Antigravity IDE', value: 'antigravity' },
206
219
  { name: 'VS Code', value: 'code' },
207
220
  { name: 'WebStorm', value: 'webstorm' },
208
221
  ];
@@ -216,7 +229,7 @@ function selectEditor() {
216
229
  output: process.stdout,
217
230
  });
218
231
  return new Promise((resolve) => {
219
- rl.question('\nEnter number (1-3, default: 1 for Cursor): ', (answer) => {
232
+ rl.question('\nEnter number (1-5, default: 1 for Cursor): ', (answer) => {
220
233
  rl.close();
221
234
  const choice = parseInt(answer.trim(), 10) || 1;
222
235
  const selected = availableEditors[Math.max(0, Math.min(choice - 1, availableEditors.length - 1))];
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import childProcess from 'child_process';
5
5
  import http from 'http';
6
6
  import { spawn } from 'child_process';
7
+ import crypto from 'crypto';
7
8
  import { fileURLToPath } from 'url';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
@@ -31,8 +32,13 @@ const cfgScan = cfg.scan ?? {};
31
32
  const scanIncludeGlobs = cfgScan.includeGlobs ?? DEFAULT_INCLUDE_GLOBS;
32
33
  const scanWorkspaceRoot = cfg.workspaceRoot?.trim() || '.';
33
34
  const cfgPort = cfg.port;
35
+ const cfgHost = cfg.host?.trim();
34
36
  const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfgPort || 4123);
37
+ const HOST = process.env.OPEN_IN_EDITOR_HOST || cfgHost || '127.0.0.1';
38
+ const AUTH_TOKEN = (process.env.OPEN_IN_EDITOR_TOKEN || cfg.authToken || '').trim() || null;
39
+ const WORKSPACE_ROOT = path.resolve(root, scanWorkspaceRoot);
35
40
  const MAP_PATH = path.resolve(root, '.open-in-editor/component-map.json');
41
+ const SAFE_WATCH_DIR_NAMES = new Set(['node_modules', 'dist', '.git', '.angular', 'coverage']);
36
42
  const editorCLICache = {};
37
43
  function checkEditorCLI(editorName, cliCommand = editorName) {
38
44
  if (editorCLICache[editorName] !== undefined)
@@ -49,12 +55,14 @@ function checkEditorCLI(editorName, cliCommand = editorName) {
49
55
  return editorCLICache[editorName];
50
56
  }
51
57
  const MAC_APP_NAMES = {
58
+ antigravity: 'Antigravity IDE',
52
59
  cursor: 'Cursor',
53
60
  code: 'Visual Studio Code',
61
+ zed: 'Zed',
54
62
  webstorm: 'WebStorm',
55
63
  };
56
64
  function detectAvailableEditors() {
57
- const editors = ['cursor', 'code', 'webstorm'];
65
+ const editors = ['cursor', 'zed', 'antigravity', 'code', 'webstorm'];
58
66
  const available = [];
59
67
  for (const editor of editors) {
60
68
  if (checkEditorCLI(editor)) {
@@ -64,9 +72,18 @@ function detectAvailableEditors() {
64
72
  return available;
65
73
  }
66
74
  const AVAILABLE_EDITORS = detectAvailableEditors();
67
- const DEFAULT_EDITOR = process.env.LAUNCH_EDITOR || cfg.editor || AVAILABLE_EDITORS[0]?.name || 'cursor';
68
- const FALLBACK_EDITOR = cfg.fallbackEditor || AVAILABLE_EDITORS[1]?.name || 'code';
75
+ const DEFAULT_EDITOR = process.env.LAUNCH_EDITOR || cfg.editor || 'cursor';
76
+ const FALLBACK_EDITOR = cfg.fallbackEditor ||
77
+ AVAILABLE_EDITORS.find((editor) => editor.name !== DEFAULT_EDITOR)?.name ||
78
+ 'code';
69
79
  const COMMAND_TEMPLATES = {
80
+ antigravity: (file) => {
81
+ if (checkEditorCLI('antigravity')) {
82
+ return ['antigravity', ['--goto', file]];
83
+ }
84
+ const filePath = file.split(':')[0];
85
+ return ['open', ['-a', MAC_APP_NAMES.antigravity, filePath]];
86
+ },
70
87
  cursor: (file) => {
71
88
  if (checkEditorCLI('cursor')) {
72
89
  return ['cursor', ['--goto', file]];
@@ -81,6 +98,13 @@ const COMMAND_TEMPLATES = {
81
98
  const filePath = file.split(':')[0];
82
99
  return ['open', ['-a', MAC_APP_NAMES.code, filePath]];
83
100
  },
101
+ zed: (file) => {
102
+ if (checkEditorCLI('zed')) {
103
+ return ['zed', [file]];
104
+ }
105
+ const filePath = file.split(':')[0];
106
+ return ['open', ['-a', MAC_APP_NAMES.zed, filePath]];
107
+ },
84
108
  webstorm: (file) => {
85
109
  if (checkEditorCLI('webstorm')) {
86
110
  const [filePath, line, col] = file.split(':');
@@ -165,6 +189,57 @@ function findBestLineInFile(filePath, searchTerms) {
165
189
  return 1;
166
190
  }
167
191
  }
192
+ function getWatchRoots(includeGlobs, workspaceRoot) {
193
+ const roots = new Set();
194
+ includeGlobs.forEach((glob) => {
195
+ const base = globToBaseDir(glob);
196
+ const resolved = path.resolve(root, workspaceRoot, base);
197
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
198
+ roots.add(resolved);
199
+ }
200
+ });
201
+ return Array.from(roots);
202
+ }
203
+ function globToBaseDir(glob) {
204
+ const wildcardIndex = glob.search(/[*?[\]{]/);
205
+ if (wildcardIndex === -1)
206
+ return glob;
207
+ const prefix = glob.slice(0, wildcardIndex);
208
+ if (prefix.endsWith('/'))
209
+ return prefix.slice(0, -1);
210
+ return path.dirname(prefix);
211
+ }
212
+ function collectNestedDirectories(baseDir, out = new Set()) {
213
+ if (!fs.existsSync(baseDir))
214
+ return out;
215
+ let stat;
216
+ try {
217
+ stat = fs.statSync(baseDir);
218
+ }
219
+ catch {
220
+ return out;
221
+ }
222
+ if (!stat.isDirectory())
223
+ return out;
224
+ if (out.has(baseDir))
225
+ return out;
226
+ out.add(baseDir);
227
+ let entries = [];
228
+ try {
229
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
230
+ }
231
+ catch {
232
+ return out;
233
+ }
234
+ for (const entry of entries) {
235
+ if (!entry.isDirectory())
236
+ continue;
237
+ if (SAFE_WATCH_DIR_NAMES.has(entry.name))
238
+ continue;
239
+ collectNestedDirectories(path.join(baseDir, entry.name), out);
240
+ }
241
+ return out;
242
+ }
168
243
  function startScanWatch() {
169
244
  const scanScript = path.resolve(__dirname, 'cmp-scan.js');
170
245
  if (!fs.existsSync(scanScript)) {
@@ -176,8 +251,8 @@ function startScanWatch() {
176
251
  console.log('[file-opener] watch roots not found, watch disabled.');
177
252
  return;
178
253
  }
179
- const recursive = process.platform === 'darwin' || process.platform === 'win32';
180
- const watchers = [];
254
+ const supportsRecursive = process.platform === 'darwin' || process.platform === 'win32';
255
+ const watcherByPath = new Map();
181
256
  let scanRunning = false;
182
257
  let scanQueued = false;
183
258
  let timer = null;
@@ -210,52 +285,111 @@ function startScanWatch() {
210
285
  const scheduleScan = (reason) => {
211
286
  if (timer)
212
287
  clearTimeout(timer);
213
- timer = setTimeout(() => runScan(reason), 500);
288
+ timer = setTimeout(() => runScan(reason), 400);
289
+ };
290
+ const refreshWatchers = () => {
291
+ if (supportsRecursive)
292
+ return;
293
+ const nextPaths = new Set();
294
+ for (const baseRoot of roots) {
295
+ const dirs = collectNestedDirectories(baseRoot);
296
+ for (const dir of dirs) {
297
+ nextPaths.add(dir);
298
+ }
299
+ }
300
+ for (const [watchPath, watcher] of watcherByPath.entries()) {
301
+ if (!nextPaths.has(watchPath)) {
302
+ watcher.close();
303
+ watcherByPath.delete(watchPath);
304
+ }
305
+ }
306
+ for (const watchPath of nextPaths) {
307
+ attachWatcher(watchPath);
308
+ }
214
309
  };
215
310
  const attachWatcher = (watchPath) => {
311
+ if (watcherByPath.has(watchPath))
312
+ return;
216
313
  try {
217
- const watcher = fs.watch(watchPath, { recursive }, (eventType, filename) => {
314
+ const watcher = fs.watch(watchPath, { recursive: supportsRecursive }, (eventType, filename) => {
218
315
  const detail = filename ? `${eventType}:${filename.toString()}` : eventType;
219
316
  scheduleScan(detail);
317
+ if (!supportsRecursive && eventType === 'rename') {
318
+ refreshWatchers();
319
+ }
220
320
  });
221
- watchers.push(watcher);
321
+ watcherByPath.set(watchPath, watcher);
222
322
  }
223
323
  catch (err) {
224
324
  const message = err instanceof Error ? err.message : String(err);
225
325
  console.log(`[file-opener] failed to watch ${watchPath}: ${message}`);
226
- throw err;
227
326
  }
228
327
  };
229
- try {
328
+ if (supportsRecursive) {
230
329
  roots.forEach(attachWatcher);
231
- console.log(`[file-opener] watch enabled (${recursive ? 'recursive' : 'non-recursive'}): ${roots.join(', ')}`);
330
+ console.log(`[file-opener] watch enabled (recursive): ${roots.join(', ')}`);
232
331
  }
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);
332
+ else {
333
+ refreshWatchers();
334
+ console.log(`[file-opener] watch enabled (directory-mode): ${watcherByPath.size} directories`);
335
+ setInterval(refreshWatchers, 10000);
237
336
  }
238
337
  runScan('initial');
239
338
  }
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);
339
+ function safeCompareToken(actual, expected) {
340
+ const actualBuffer = Buffer.from(actual);
341
+ const expectedBuffer = Buffer.from(expected);
342
+ if (actualBuffer.length !== expectedBuffer.length)
343
+ return false;
344
+ return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
250
345
  }
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);
346
+ function extractRequestToken(req, url) {
347
+ const queryToken = url.searchParams.get('token');
348
+ if (queryToken)
349
+ return queryToken;
350
+ const headerToken = req.headers['x-locatorjs-token'];
351
+ if (typeof headerToken === 'string' && headerToken.trim().length > 0) {
352
+ return headerToken.trim();
353
+ }
354
+ const authorization = req.headers.authorization;
355
+ if (typeof authorization === 'string' && authorization.startsWith('Bearer ')) {
356
+ return authorization.slice('Bearer '.length).trim();
357
+ }
358
+ return null;
359
+ }
360
+ function ensureAuthorized(req, url) {
361
+ if (!AUTH_TOKEN)
362
+ return true;
363
+ const token = extractRequestToken(req, url);
364
+ if (!token)
365
+ return false;
366
+ return safeCompareToken(token, AUTH_TOKEN);
367
+ }
368
+ function resolveAllowedFilePath(rawPath) {
369
+ const trimmed = rawPath.trim();
370
+ if (!trimmed) {
371
+ throw new Error('file is required');
372
+ }
373
+ if (trimmed.includes('\0')) {
374
+ throw new Error('file path is invalid');
375
+ }
376
+ const resolved = path.isAbsolute(trimmed)
377
+ ? path.resolve(trimmed)
378
+ : path.resolve(WORKSPACE_ROOT, trimmed);
379
+ if (!fs.existsSync(resolved)) {
380
+ throw new Error('file does not exist');
381
+ }
382
+ const realWorkspaceRoot = fs.realpathSync(WORKSPACE_ROOT);
383
+ const realPath = fs.realpathSync(resolved);
384
+ const relative = path.relative(realWorkspaceRoot, realPath);
385
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
386
+ throw new Error('file path is outside workspace root');
387
+ }
388
+ const stat = fs.statSync(realPath);
389
+ if (!stat.isFile()) {
390
+ throw new Error('path must be a file');
391
+ }
392
+ return realPath;
259
393
  }
260
394
  const server = http.createServer((req, res) => {
261
395
  if (!req.url) {
@@ -270,6 +404,11 @@ const server = http.createServer((req, res) => {
270
404
  }
271
405
  const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
272
406
  const pathname = url.pathname;
407
+ if (!ensureAuthorized(req, url)) {
408
+ res.statusCode = 401;
409
+ res.end('Unauthorized request');
410
+ return;
411
+ }
273
412
  if (pathname === '/__cmp-map') {
274
413
  if (!fs.existsSync(MAP_PATH)) {
275
414
  res.statusCode = 404;
@@ -289,9 +428,18 @@ const server = http.createServer((req, res) => {
289
428
  res.end('file is required');
290
429
  return;
291
430
  }
292
- const decoded = decodeURIComponent(file);
293
- console.log(`[file-opener] Opening file: ${decoded}:${line}:${col}`);
294
- const fileWithPos = `${decoded}:${line}:${col}`;
431
+ let safePath;
432
+ try {
433
+ safePath = resolveAllowedFilePath(decodeURIComponent(file));
434
+ }
435
+ catch (error) {
436
+ const message = error instanceof Error ? error.message : 'invalid file';
437
+ res.statusCode = 403;
438
+ res.end(message);
439
+ return;
440
+ }
441
+ console.log(`[file-opener] Opening file: ${safePath}:${line}:${col}`);
442
+ const fileWithPos = `${safePath}:${line}:${col}`;
295
443
  const ok = launchInEditor(fileWithPos);
296
444
  if (!ok) {
297
445
  res.statusCode = 500;
@@ -314,11 +462,25 @@ const server = http.createServer((req, res) => {
314
462
  res.end('search terms required');
315
463
  return;
316
464
  }
317
- const decoded = decodeURIComponent(file);
465
+ let safePath;
466
+ try {
467
+ safePath = resolveAllowedFilePath(decodeURIComponent(file));
468
+ }
469
+ catch (error) {
470
+ const message = error instanceof Error ? error.message : 'invalid file';
471
+ res.statusCode = 403;
472
+ res.end(message);
473
+ return;
474
+ }
318
475
  try {
319
476
  const searchTerms = JSON.parse(decodeURIComponent(searchParam));
320
- const bestLine = findBestLineInFile(decoded, searchTerms);
321
- const fileWithPos = `${decoded}:${bestLine}:1`;
477
+ if (!Array.isArray(searchTerms) || !searchTerms.every((term) => typeof term === 'string')) {
478
+ res.statusCode = 400;
479
+ res.end('search terms must be string[]');
480
+ return;
481
+ }
482
+ const bestLine = findBestLineInFile(safePath, searchTerms);
483
+ const fileWithPos = `${safePath}:${bestLine}:1`;
322
484
  const ok = launchInEditor(fileWithPos);
323
485
  if (!ok) {
324
486
  res.statusCode = 500;
@@ -339,10 +501,17 @@ const server = http.createServer((req, res) => {
339
501
  res.end('Not found');
340
502
  });
341
503
  server
342
- .listen(PORT, () => {
343
- console.log(`[file-opener] http://localhost:${PORT}`);
504
+ .listen(PORT, HOST, () => {
505
+ console.log(`[file-opener] http://${HOST}:${PORT}`);
344
506
  console.log(` - map: ${path.relative(root, MAP_PATH)}`);
345
507
  console.log(` - editor: ${DEFAULT_EDITOR} (fallback: ${FALLBACK_EDITOR})`);
508
+ console.log(` - workspace root: ${WORKSPACE_ROOT}`);
509
+ if (AUTH_TOKEN) {
510
+ console.log(' - auth: enabled');
511
+ }
512
+ else {
513
+ console.log(' - auth: disabled (legacy config, run locatorjs-config to enable)');
514
+ }
346
515
  if (AVAILABLE_EDITORS.length > 0) {
347
516
  console.log(' - detected editors:');
348
517
  AVAILABLE_EDITORS.forEach((editor) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-locatorjs",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "LocatorJs open-in-editor tools for Angular projects",
5
5
  "keywords": [
6
6
  "angular",
@@ -49,11 +49,14 @@
49
49
  "build:node": "tsc -p tsconfig.node.json",
50
50
  "clean": "rm -rf dist",
51
51
  "prepare": "npm run build",
52
+ "test": "node --test",
53
+ "test:ci": "npm run build && npm test",
52
54
  "format": "oxfmt",
53
- "format:check": "oxfmt --check"
55
+ "format:check": "oxfmt --check",
56
+ "publish:github": "node scripts/publish-github.mjs"
54
57
  },
55
58
  "dependencies": {
56
- "glob": "^11.0.0"
59
+ "glob": "^13.0.6"
57
60
  },
58
61
  "devDependencies": {
59
62
  "@types/node": "^18.18.0",