ngx-locatorjs 0.3.0 → 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
@@ -15,10 +15,9 @@
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 {proxyConfigPath}`
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 {proxyConfigPath}`
21
- `{proxyConfigPath}`는 `npx locatorjs-config`가 선택/병합한 실제 프록시 파일 경로로 바꿔서 사용하세요.
20
+ 기존 0.3.0 버전에서 업그레이드했다면 `npx locatorjs-config`를 다시 실행해 `authToken`과 proxy 헤더를 재생성하고, `--proxy-config`(또는 `angular.json`)를 `ngx-locatorjs.proxy.cjs`로 맞추세요.
22
21
 
23
22
  **Angular 코드 추가 (main.ts)**
24
23
 
@@ -65,10 +64,10 @@ bootstrapApplication(AppComponent, appConfig)
65
64
  **Angular dev server 예시**
66
65
 
67
66
  - CLI 실행
68
- `ng serve --proxy-config {proxyConfigPath}`
67
+ `ng serve --proxy-config ngx-locatorjs.proxy.cjs`
69
68
 
70
69
  - angular.json에 적용
71
- `"serve"` 옵션에 `"proxyConfig": "{proxyConfigPath}"` 추가
70
+ `"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.cjs"` 추가
72
71
 
73
72
  **컴포넌트 맵 스캔**
74
73
 
@@ -94,7 +93,7 @@ bootstrapApplication(AppComponent, appConfig)
94
93
  **중요**
95
94
 
96
95
  - `npx locatorjs-config`는 **실행한 현재 폴더를 기준**으로 설정합니다.
97
- - 기본값: `port: 4123`, `workspaceRoot: "."`.
96
+ - 기본값: `host: "127.0.0.1"`, `port: 4123`, `workspaceRoot: "."`.
98
97
  - 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 `workspaceRoot`를 그 **상대 경로**로 수정하세요. (예: `apps/web`)
99
98
  - `.gitignore`가 있으면 `npx locatorjs-config`가 `.open-in-editor/`를 자동 추가합니다. 커밋하려면 해당 항목을 제거하세요.
100
99
 
@@ -102,10 +101,12 @@ bootstrapApplication(AppComponent, appConfig)
102
101
 
103
102
  ```json
104
103
  {
104
+ "host": "127.0.0.1",
105
105
  "port": 4123,
106
106
  "workspaceRoot": ".",
107
107
  "editor": "cursor",
108
108
  "fallbackEditor": "code",
109
+ "authToken": "locatorjs-config가 자동 생성",
109
110
  "scan": {
110
111
  "includeGlobs": [
111
112
  "src/**/*.{ts,tsx}",
@@ -129,9 +130,11 @@ bootstrapApplication(AppComponent, appConfig)
129
130
  **필드 설명**
130
131
 
131
132
  - `port`: 로컬 file-opener 서버 포트입니다.
133
+ - `host`: file-opener 서버 바인드 주소입니다. 로컬 전용으로 `127.0.0.1` 사용을 권장합니다.
132
134
  - `workspaceRoot`: 명령 실행 위치 기준 Angular 워크스페이스 루트 상대 경로입니다.
133
- - `editor`: 기본 에디터입니다 (`cursor`, `zed`, `antigravity`, `code`, `webstorm`).
135
+ - `editor`: 기본 에디터입니다 (`cursor`, `code`, `webstorm`).
134
136
  - `fallbackEditor`: 기본 에디터 실행 실패 시 사용할 대체 에디터입니다.
137
+ - `authToken`: open-in-editor 서버 요청 검증 토큰입니다. `locatorjs-config`가 자동 생성합니다.
135
138
  - `scan.includeGlobs`: 컴포넌트 소스 파일 탐색 대상 glob 목록입니다.
136
139
  - `scan.excludeGlobs`: 컴포넌트 스캔에서 제외할 glob 목록입니다.
137
140
 
@@ -151,41 +154,25 @@ bootstrapApplication(AppComponent, appConfig)
151
154
  3. `ngx-locatorjs.config.json`의 `editor`
152
155
  4. 자동 감지된 에디터
153
156
 
154
- **프록시 설정 ({proxyConfigPath})**
155
- `{proxyConfigPath}`는 `npx locatorjs-config` 실행 시 아래 규칙으로 결정됩니다.
156
-
157
- - `angular.json`에 `proxyConfig`가 지정되어 있으면 그 파일에 병합합니다.
158
- - 없고 `proxy.conf.json`이 있으면 그 파일에 병합합니다.
159
- - 둘 다 없으면 `ngx-locatorjs.proxy.json`을 생성합니다.
157
+ **프록시 설정 (ngx-locatorjs.proxy.cjs)**
158
+ `npx locatorjs-config` 실행 시 자동 생성됩니다. 이 파일은 실행 시점에 `ngx-locatorjs.config.json`을 읽어 target을 구성하므로, 포트를 바꾸려면 `ngx-locatorjs.config.json`의 `port`만 수정하면 됩니다.
160
159
 
161
160
  예시:
162
161
 
163
- ```json
164
- {
165
- "/__open-in-editor": {
166
- "target": "http://localhost:4123",
167
- "secure": false,
168
- "changeOrigin": true
169
- },
170
- "/__open-in-editor-search": {
171
- "target": "http://localhost:4123",
172
- "secure": false,
173
- "changeOrigin": true
174
- },
175
- "/__cmp-map": {
176
- "target": "http://localhost:4123",
177
- "secure": false,
178
- "changeOrigin": true
179
- }
180
- }
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
+ };
181
168
  ```
182
169
 
183
170
  **트러블슈팅**
184
171
 
185
172
  1. CORS 에러
186
- `ng serve --proxy-config {proxyConfigPath}` 사용 여부 확인
173
+ `ng serve --proxy-config ngx-locatorjs.proxy.cjs` 사용 여부 확인
187
174
  2. npm run 경고
188
- `npm run start -- --proxy-config {proxyConfigPath}` 형태로 실행
175
+ `npm run start -- --proxy-config ngx-locatorjs.proxy.cjs` 형태로 실행
189
176
  3. 네트워크 비활성
190
177
  `installAngularLocator({ enableNetwork: true })` 설정 확인
191
178
  4. component-map.json not found
@@ -199,31 +186,20 @@ bootstrapApplication(AppComponent, appConfig)
199
186
  8. 하이라이트가 안 보이거나 info가 null로 나옴
200
187
  `http://localhost:${port}/__cmp-map` 에서 컴포넌트 정보가 잘 나타나는지 확인
201
188
  9. 포트 충돌
202
- `ngx-locatorjs.config.json`과 `{proxyConfigPath}`에서 포트 일치 여부 확인
189
+ `ngx-locatorjs.config.json`의 `port`만 수정하면 됩니다
190
+ 10. opener 라우트에서 401 발생
191
+ `npx locatorjs-config` 재실행으로 토큰/프록시 헤더를 다시 생성하거나, 프록시 없이 직접 호출 시 `installAngularLocator`에 `authToken` 전달
203
192
 
204
193
  **주의**
205
194
 
206
195
  - 개발 모드에서만 사용하세요. 프로덕션 번들에 포함되지 않도록 `environment.production` 체크를 권장합니다.
207
196
  - 네트워크 요청은 opt-in이며 localhost로만 제한됩니다. `enableNetwork: true`로 활성화하세요.
197
+ - opener 서버는 기본적으로 loopback(`127.0.0.1`)에만 바인드되며, `workspaceRoot` 바깥 파일은 열 수 없습니다.
208
198
 
209
199
  **원 커맨드 실행 (추천)**
210
- file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식 중 하나를 사용하세요.
211
-
212
- ### Option A: `concurrently`
213
-
214
- ```bash
215
- npm i -D concurrently
216
- ```
217
-
218
- ```json
219
- {
220
- "scripts": {
221
- "dev:locator": "concurrently -k -n opener,ng \"npx locatorjs-open-in-editor\" \"ng serve --proxy-config {proxyConfigPath}\""
222
- }
223
- }
224
- ```
200
+ file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식을 사용하세요.
225
201
 
226
- ### Option B: `npm-run-all`
202
+ ### `npm-run-all`
227
203
 
228
204
  ```bash
229
205
  npm i -D npm-run-all
@@ -233,7 +209,7 @@ npm i -D npm-run-all
233
209
  {
234
210
  "scripts": {
235
211
  "locator:opener": "npx locatorjs-open-in-editor",
236
- "dev:app": "ng serve --proxy-config {proxyConfigPath}",
212
+ "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.cjs",
237
213
  "dev:locator": "run-p locator:opener dev:app"
238
214
  }
239
215
  }
package/README.md CHANGED
@@ -32,10 +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 {proxyConfigPath}`
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 {proxyConfigPath}`
38
- Replace `{proxyConfigPath}` with the actual proxy file path selected/updated by `npx locatorjs-config`.
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`.
39
38
 
40
39
  ## Add to `main.ts`
41
40
 
@@ -94,7 +93,7 @@ Location: project root
94
93
  **Important**
95
94
 
96
95
  - `npx locatorjs-config` uses the **current directory** as the base.
97
- - Defaults: `port: 4123`, `workspaceRoot: "."`.
96
+ - Defaults: `host: "127.0.0.1"`, `port: 4123`, `workspaceRoot: "."`.
98
97
  - In a monorepo, update `workspaceRoot` to the **relative path** of your Angular app (e.g. `apps/web`).
99
98
  - If `.gitignore` exists, `npx locatorjs-config` will append `.open-in-editor/`. Remove it if you want to commit the map.
100
99
 
@@ -102,10 +101,12 @@ Example:
102
101
 
103
102
  ```json
104
103
  {
104
+ "host": "127.0.0.1",
105
105
  "port": 4123,
106
106
  "workspaceRoot": ".",
107
107
  "editor": "cursor",
108
108
  "fallbackEditor": "code",
109
+ "authToken": "generated-by-locatorjs-config",
109
110
  "scan": {
110
111
  "includeGlobs": [
111
112
  "src/**/*.{ts,tsx}",
@@ -129,9 +130,11 @@ Example:
129
130
  ### Field Reference
130
131
 
131
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.
132
134
  - `workspaceRoot`: Angular workspace root path (relative to where you run commands).
133
- - `editor`: Preferred editor (`cursor`, `zed`, `antigravity`, `code`, `webstorm`).
135
+ - `editor`: Preferred editor (`cursor`, `code`, `webstorm`).
134
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`.
135
138
  - `scan.includeGlobs`: Globs used to find component source files.
136
139
  - `scan.excludeGlobs`: Globs excluded from component scanning.
137
140
 
@@ -141,34 +144,18 @@ Example:
141
144
  - Angular workspace: `"projects/**/*.{ts,tsx}"`
142
145
  - Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
143
146
 
144
- ## Proxy (`{proxyConfigPath}`)
147
+ ## Proxy (`ngx-locatorjs.proxy.cjs`)
145
148
 
146
- `{proxyConfigPath}` is decided by `npx locatorjs-config`:
147
-
148
- - If `angular.json` already specifies `proxyConfig`, it updates that file.
149
- - Else if `proxy.conf.json` exists, it updates that file.
150
- - 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.
151
150
 
152
151
  Example:
153
152
 
154
- ```json
155
- {
156
- "/__open-in-editor": {
157
- "target": "http://localhost:4123",
158
- "secure": false,
159
- "changeOrigin": true
160
- },
161
- "/__open-in-editor-search": {
162
- "target": "http://localhost:4123",
163
- "secure": false,
164
- "changeOrigin": true
165
- },
166
- "/__cmp-map": {
167
- "target": "http://localhost:4123",
168
- "secure": false,
169
- "changeOrigin": true
170
- }
171
- }
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
+ };
172
159
  ```
173
160
 
174
161
  ## Environment Variable Priority
@@ -180,40 +167,28 @@ Example:
180
167
 
181
168
  ## Troubleshooting
182
169
 
183
- - **CORS / JSON parse error**: ensure dev server uses `--proxy-config {proxyConfigPath}`
184
- - **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config {proxyConfigPath}`
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`
185
172
  - **Network disabled**: pass `enableNetwork: true` to `installAngularLocator`
186
173
  - **component-map.json not found**: run `npx locatorjs-scan`
187
174
  - **Component changes not reflected**: run `npx locatorjs-open-in-editor --watch` or re-run `npx locatorjs-scan`
188
175
  - **Map is empty or missing components**: check `scan.includeGlobs` and rerun the scan
189
176
  - **Wrong files open or nothing matches**: confirm `workspaceRoot` points to the actual Angular app root
190
177
  - **No highlight / info is null**: make sure `http://localhost:${port}/__cmp-map` is loading and includes your component class name
191
- - **Port conflict**: change port in both `ngx-locatorjs.config.json` and `{proxyConfigPath}`
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
192
180
 
193
181
  ## Notes
194
182
 
195
183
  - Use only in development (guard with `environment.production`).
196
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.
197
186
 
198
187
  ## One-Command Dev (Recommended)
199
188
 
200
189
  Running the file-opener server and Angular dev server separately is tedious. You can wire them into a single script.
201
190
 
202
- ### Option A: `concurrently`
203
-
204
- ```bash
205
- npm i -D concurrently
206
- ```
207
-
208
- ```json
209
- {
210
- "scripts": {
211
- "dev:locator": "concurrently -k -n opener,ng \"npx locatorjs-open-in-editor\" \"ng serve --proxy-config {proxyConfigPath}\""
212
- }
213
- }
214
- ```
215
-
216
- ### Option B: `npm-run-all`
191
+ ### `npm-run-all`
217
192
 
218
193
  ```bash
219
194
  npm i -D npm-run-all
@@ -223,7 +198,7 @@ npm i -D npm-run-all
223
198
  {
224
199
  "scripts": {
225
200
  "locator:opener": "npx locatorjs-open-in-editor",
226
- "dev:app": "ng serve --proxy-config {proxyConfigPath}",
201
+ "dev:app": "ng serve --proxy-config ngx-locatorjs.proxy.cjs",
227
202
  "dev:locator": "run-p locator:opener dev:app"
228
203
  }
229
204
  }
@@ -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,8 +205,12 @@ 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' },
@@ -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)
@@ -183,6 +189,57 @@ function findBestLineInFile(filePath, searchTerms) {
183
189
  return 1;
184
190
  }
185
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
+ }
186
243
  function startScanWatch() {
187
244
  const scanScript = path.resolve(__dirname, 'cmp-scan.js');
188
245
  if (!fs.existsSync(scanScript)) {
@@ -194,8 +251,8 @@ function startScanWatch() {
194
251
  console.log('[file-opener] watch roots not found, watch disabled.');
195
252
  return;
196
253
  }
197
- const recursive = process.platform === 'darwin' || process.platform === 'win32';
198
- const watchers = [];
254
+ const supportsRecursive = process.platform === 'darwin' || process.platform === 'win32';
255
+ const watcherByPath = new Map();
199
256
  let scanRunning = false;
200
257
  let scanQueued = false;
201
258
  let timer = null;
@@ -228,52 +285,111 @@ function startScanWatch() {
228
285
  const scheduleScan = (reason) => {
229
286
  if (timer)
230
287
  clearTimeout(timer);
231
- 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
+ }
232
309
  };
233
310
  const attachWatcher = (watchPath) => {
311
+ if (watcherByPath.has(watchPath))
312
+ return;
234
313
  try {
235
- const watcher = fs.watch(watchPath, { recursive }, (eventType, filename) => {
314
+ const watcher = fs.watch(watchPath, { recursive: supportsRecursive }, (eventType, filename) => {
236
315
  const detail = filename ? `${eventType}:${filename.toString()}` : eventType;
237
316
  scheduleScan(detail);
317
+ if (!supportsRecursive && eventType === 'rename') {
318
+ refreshWatchers();
319
+ }
238
320
  });
239
- watchers.push(watcher);
321
+ watcherByPath.set(watchPath, watcher);
240
322
  }
241
323
  catch (err) {
242
324
  const message = err instanceof Error ? err.message : String(err);
243
325
  console.log(`[file-opener] failed to watch ${watchPath}: ${message}`);
244
- throw err;
245
326
  }
246
327
  };
247
- try {
328
+ if (supportsRecursive) {
248
329
  roots.forEach(attachWatcher);
249
- console.log(`[file-opener] watch enabled (${recursive ? 'recursive' : 'non-recursive'}): ${roots.join(', ')}`);
330
+ console.log(`[file-opener] watch enabled (recursive): ${roots.join(', ')}`);
250
331
  }
251
- catch {
252
- watchers.forEach((w) => w.close());
253
- console.log('[file-opener] falling back to polling scan every 5s');
254
- setInterval(() => runScan('poll'), 5000);
332
+ else {
333
+ refreshWatchers();
334
+ console.log(`[file-opener] watch enabled (directory-mode): ${watcherByPath.size} directories`);
335
+ setInterval(refreshWatchers, 10000);
255
336
  }
256
337
  runScan('initial');
257
338
  }
258
- function getWatchRoots(includeGlobs, workspaceRoot) {
259
- const roots = new Set();
260
- includeGlobs.forEach((glob) => {
261
- const base = globToBaseDir(glob);
262
- const resolved = path.resolve(root, workspaceRoot, base);
263
- if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
264
- roots.add(resolved);
265
- }
266
- });
267
- 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);
268
345
  }
269
- function globToBaseDir(glob) {
270
- const wildcardIndex = glob.search(/[*?[\]{]/);
271
- if (wildcardIndex === -1)
272
- return glob;
273
- const prefix = glob.slice(0, wildcardIndex);
274
- if (prefix.endsWith('/'))
275
- return prefix.slice(0, -1);
276
- 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;
277
393
  }
278
394
  const server = http.createServer((req, res) => {
279
395
  if (!req.url) {
@@ -288,6 +404,11 @@ const server = http.createServer((req, res) => {
288
404
  }
289
405
  const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
290
406
  const pathname = url.pathname;
407
+ if (!ensureAuthorized(req, url)) {
408
+ res.statusCode = 401;
409
+ res.end('Unauthorized request');
410
+ return;
411
+ }
291
412
  if (pathname === '/__cmp-map') {
292
413
  if (!fs.existsSync(MAP_PATH)) {
293
414
  res.statusCode = 404;
@@ -307,9 +428,18 @@ const server = http.createServer((req, res) => {
307
428
  res.end('file is required');
308
429
  return;
309
430
  }
310
- const decoded = decodeURIComponent(file);
311
- console.log(`[file-opener] Opening file: ${decoded}:${line}:${col}`);
312
- 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}`;
313
443
  const ok = launchInEditor(fileWithPos);
314
444
  if (!ok) {
315
445
  res.statusCode = 500;
@@ -332,11 +462,25 @@ const server = http.createServer((req, res) => {
332
462
  res.end('search terms required');
333
463
  return;
334
464
  }
335
- 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
+ }
336
475
  try {
337
476
  const searchTerms = JSON.parse(decodeURIComponent(searchParam));
338
- const bestLine = findBestLineInFile(decoded, searchTerms);
339
- 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`;
340
484
  const ok = launchInEditor(fileWithPos);
341
485
  if (!ok) {
342
486
  res.statusCode = 500;
@@ -357,10 +501,17 @@ const server = http.createServer((req, res) => {
357
501
  res.end('Not found');
358
502
  });
359
503
  server
360
- .listen(PORT, () => {
361
- console.log(`[file-opener] http://localhost:${PORT}`);
504
+ .listen(PORT, HOST, () => {
505
+ console.log(`[file-opener] http://${HOST}:${PORT}`);
362
506
  console.log(` - map: ${path.relative(root, MAP_PATH)}`);
363
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
+ }
364
515
  if (AVAILABLE_EDITORS.length > 0) {
365
516
  console.log(' - detected editors:');
366
517
  AVAILABLE_EDITORS.forEach((editor) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-locatorjs",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "LocatorJs open-in-editor tools for Angular projects",
5
5
  "keywords": [
6
6
  "angular",
@@ -49,8 +49,11 @@
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
59
  "glob": "^13.0.6"