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 +34 -46
- package/README.md +32 -43
- 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 +66 -53
- package/dist/node/file-opener.js +210 -41
- package/package.json +6 -3
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.
|
|
18
|
+
4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.cjs`
|
|
19
19
|
|
|
20
|
-
|
|
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.
|
|
67
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.cjs`
|
|
68
68
|
|
|
69
69
|
- angular.json에 적용
|
|
70
|
-
`"serve"` 옵션에 `"proxyConfig": "ngx-locatorjs.proxy.
|
|
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.
|
|
154
|
-
`npx locatorjs-config` 실행 시 자동 생성됩니다.
|
|
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
|
-
```
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
173
|
+
`ng serve --proxy-config ngx-locatorjs.proxy.cjs` 사용 여부 확인
|
|
182
174
|
2. npm run 경고
|
|
183
|
-
`npm run start -- --proxy-config ngx-locatorjs.proxy.
|
|
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
|
|
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
|
-
###
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
147
|
+
## Proxy (`ngx-locatorjs.proxy.cjs`)
|
|
144
148
|
|
|
145
|
-
Generated by `npx locatorjs-config`.
|
|
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
|
-
```
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
179
|
-
- **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.
|
|
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
|
|
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
|
-
###
|
|
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.
|
|
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.
|
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,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-
|
|
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))];
|
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)
|
|
@@ -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 ||
|
|
68
|
-
const FALLBACK_EDITOR = cfg.fallbackEditor ||
|
|
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
|
|
180
|
-
const
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
if (supportsRecursive) {
|
|
230
329
|
roots.forEach(attachWatcher);
|
|
231
|
-
console.log(`[file-opener] watch enabled (
|
|
330
|
+
console.log(`[file-opener] watch enabled (recursive): ${roots.join(', ')}`);
|
|
232
331
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.log(
|
|
236
|
-
setInterval(
|
|
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
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
return
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
return
|
|
258
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
|
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.
|
|
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": "^
|
|
59
|
+
"glob": "^13.0.6"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
62
|
"@types/node": "^18.18.0",
|