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 +27 -51
- package/README.md +23 -48
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +24 -5
- package/dist/node/cmp-scan.js +53 -12
- package/dist/node/config-setup.js +63 -52
- package/dist/node/file-opener.js +189 -38
- package/package.json +5 -2
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
|
|
18
|
+
4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.cjs`
|
|
19
19
|
|
|
20
|
-
|
|
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
|
|
67
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.cjs`
|
|
69
68
|
|
|
70
69
|
- angular.json에 적용
|
|
71
|
-
`"serve"` 옵션에 `"proxyConfig": "
|
|
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`, `
|
|
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
|
-
**프록시 설정 (
|
|
155
|
-
`
|
|
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
|
-
```
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
173
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.cjs` 사용 여부 확인
|
|
187
174
|
2. npm run 경고
|
|
188
|
-
`npm run start -- --proxy-config
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
|
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`, `
|
|
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 (`
|
|
147
|
+
## Proxy (`ngx-locatorjs.proxy.cjs`)
|
|
145
148
|
|
|
146
|
-
|
|
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
|
-
```
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
184
|
-
- **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAChD,CAAC;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;
|
|
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"}
|
package/dist/browser/index.js
CHANGED
|
@@ -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,
|
package/dist/node/cmp-scan.js
CHANGED
|
@@ -110,10 +110,31 @@ async function main() {
|
|
|
110
110
|
fs.mkdirSync(outDir, { recursive: true });
|
|
111
111
|
function loadCache() {
|
|
112
112
|
try {
|
|
113
|
-
|
|
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
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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(
|
|
177
|
-
|
|
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.
|
|
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
|
-
|
|
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)} (
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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' },
|
package/dist/node/file-opener.js
CHANGED
|
@@ -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
|
|
198
|
-
const
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
if (supportsRecursive) {
|
|
248
329
|
roots.forEach(attachWatcher);
|
|
249
|
-
console.log(`[file-opener] watch enabled (
|
|
330
|
+
console.log(`[file-opener] watch enabled (recursive): ${roots.join(', ')}`);
|
|
250
331
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(
|
|
254
|
-
setInterval(
|
|
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
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
272
|
-
return
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
return
|
|
276
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
|
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
|
+
"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"
|