ngx-locatorjs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # ngx-locatorjs (Open-in-Editor)
2
2
 
3
3
  브라우저에서 Alt+클릭으로 Angular 컴포넌트 파일을 에디터에서 바로 여는 개발용 도구입니다. Angular 프로젝트 어디에나 npm 패키지로 설치해 사용할 수 있습니다.
4
+ 이 프로젝트는 [locatorjs.com](https://www.locatorjs.com/)에서 영감을 받았습니다.
4
5
 
5
6
  **기능**
6
7
  - Alt+클릭: 템플릿(.html) 열기
@@ -8,13 +9,13 @@
8
9
  - Alt 키 홀드: 컴포넌트 하이라이트 + 툴팁 표시
9
10
  - Cursor, VS Code, WebStorm 지원
10
11
 
11
- **필수 단계 (1~5 반드시 수행)**
12
+ **필수 단계 (1~4 반드시 수행)**
12
13
  1. 패키지 설치: `npm i -D ngx-locatorjs`
13
- 2. `main.ts`에 런타임 추가 (아래 예시 참고)
14
- 3. 설정/프록시 생성: `npx locatorjs-config`
15
- 4. 컴포넌트 스캔: `npx locatorjs-scan`
16
- 5. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
17
- - `npm run start` 사용 시 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
14
+ 2. 설정/프록시 생성: `npx locatorjs-config`
15
+ 3. `main.ts`에 런타임 추가 (아래 예시 참고)
16
+ 4. 파일 오프너 서버 + dev 서버 실행 (둘 다 켜진 상태 유지): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
17
+
18
+ `npm run start` 사용 시 `--` 뒤에 전달: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
18
19
 
19
20
  **Angular 코드 추가 (main.ts)**
20
21
  ```ts
@@ -32,7 +33,9 @@ platformBrowserDynamic()
32
33
  .then(() => {
33
34
  if (!environment.production) {
34
35
  setTimeout(() => {
35
- import('ngx-locatorjs').then((m) => m.installAngularLocator());
36
+ import('ngx-locatorjs').then((m) =>
37
+ m.installAngularLocator({ enableNetwork: true }),
38
+ );
36
39
  }, 1000);
37
40
  }
38
41
  })
@@ -49,7 +52,9 @@ bootstrapApplication(AppComponent, appConfig)
49
52
  .then(() => {
50
53
  setTimeout(() => {
51
54
  import('ngx-locatorjs')
52
- .then((m) => m.installAngularLocator())
55
+ .then((m) =>
56
+ m.installAngularLocator({ enableNetwork: true }),
57
+ )
53
58
  .catch((err) => console.warn('[angular-locator] Failed to load:', err));
54
59
  }, 1000);
55
60
  })
@@ -76,7 +81,6 @@ bootstrapApplication(AppComponent, appConfig)
76
81
  - 단일 Angular 앱, workspace, Nx 구조에서 동작
77
82
 
78
83
  **불가능/제한 사항**
79
- - 동적/반복 템플릿의 정확한 라인 매칭은 100% 보장 불가
80
84
  - SSR/SSG 환경에서는 동작하지 않음 (브라우저 DOM 기반)
81
85
 
82
86
  **ngx-locatorjs.config.json 가이드**
@@ -84,24 +88,30 @@ bootstrapApplication(AppComponent, appConfig)
84
88
 
85
89
  **중요**
86
90
  - `npx locatorjs-config`는 **실행한 현재 폴더를 기준**으로 설정합니다.
87
- - 프로젝트 루트에서 실행하고, `workspaceRoot` 질문에서 **Enter**를 누르면 `.`(현재 폴더)로 저장됩니다.
88
- - 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 그 **상대 경로**를 입력하세요. (예: `apps/web`)
91
+ - 기본값: `port: 4123`, `workspaceRoot: "."`.
92
+ - 모노레포처럼 실제 Angular 앱이 하위 폴더에 있으면 `workspaceRoot`를 그 **상대 경로**로 수정하세요. (예: `apps/web`)
89
93
  - `.gitignore`가 있으면 `npx locatorjs-config`가 `.open-in-editor/`를 자동 추가합니다. 커밋하려면 해당 항목을 제거하세요.
90
94
 
91
95
  예시:
92
96
  ```json
93
97
  {
94
- "port": 4123,
95
- "workspaceRoot": ".",
96
- "editor": "cursor",
97
- "fallbackEditor": "code",
98
+ "port": 4123, // 프록시 서버가 실행될 포트 주소
99
+ "workspaceRoot": ".", // Angular 워크스페이스 루트
100
+ "editor": "cursor", // 파일을 열 때 사용할 에디터 (`cursor`, `code`, `webstorm`)
101
+ "fallbackEditor": "code", // 기본 에디터 실패 시 사용할 에디터
98
102
  "scan": {
103
+ /**
104
+ * 탐색할 컴포넌트의 경로 목록
105
+ */
99
106
  "includeGlobs": [
100
107
  "src/**/*.{ts,tsx}",
101
108
  "projects/**/*.{ts,tsx}",
102
109
  "apps/**/*.{ts,tsx}",
103
110
  "libs/**/*.{ts,tsx}"
104
111
  ],
112
+ /**
113
+ * 스캔에서 제외할 경로 목록
114
+ */
105
115
  "excludeGlobs": [
106
116
  "**/node_modules/**",
107
117
  "**/dist/**",
@@ -115,15 +125,7 @@ bootstrapApplication(AppComponent, appConfig)
115
125
  }
116
126
  ```
117
127
 
118
- **필드 설명**
119
- - `port`: file-opener 서버 포트
120
- - `workspaceRoot`: 실제 Angular 워크스페이스 루트(모노레포에서 하위 폴더일 때 사용)
121
- - `editor`: 기본 에디터 (`cursor`, `code`, `webstorm`)
122
- - `fallbackEditor`: 기본 에디터 실패 시 사용할 에디터
123
- - `scan.includeGlobs`: 컴포넌트 탐색 대상 경로
124
- - `scan.excludeGlobs`: 스캔 제외 경로
125
-
126
- **프로젝트 구조별 추천 includeGlobs**
128
+ **프로젝트 구조별 includeGlobs 예시**
127
129
  1. 일반 Angular 앱
128
130
  `["src/app/**/*.ts"]`
129
131
  2. Angular Workspace (projects/)
@@ -166,21 +168,24 @@ bootstrapApplication(AppComponent, appConfig)
166
168
  `ng serve --proxy-config ngx-locatorjs.proxy.json` 사용 여부 확인
167
169
  2. npm run 경고
168
170
  `npm run start -- --proxy-config ngx-locatorjs.proxy.json` 형태로 실행
169
- 3. component-map.json not found
171
+ 3. 네트워크 비활성
172
+ `installAngularLocator({ enableNetwork: true })` 설정 확인
173
+ 4. component-map.json not found
170
174
  `npx locatorjs-scan` 실행 후 `.open-in-editor/component-map.json` 생성 여부 확인
171
- 4. 스캔 결과가 비어있거나 컴포넌트가 누락됨
172
- `scan.includeGlobs` 경로 확인 재스캔
173
- 5. 잘못된 파일이 열리거나 매칭이 안 됨
174
- `workspaceRoot`가 실제 Angular 루트인지 확인
175
- 6. 하이라이트가 보이거나 info가 null로 나옴
176
- `/__cmp-map` 응답에 컴포넌트 클래스명이 포함되는지 확인
177
- 7. 에디터가 열리지 않음
178
- CLI 설치 확인 또는 `EDITOR_CMD` 설정
179
- 8. 포트 충돌
175
+ 5. 컴포넌트 변경이 반영되지 않음
176
+ `npx locatorjs-open-in-editor --watch` 사용 또는 `npx locatorjs-scan` 재실행
177
+ 6. 스캔 결과가 비어있거나 컴포넌트가 누락됨
178
+ `scan.includeGlobs` 경로 확인 후 재스캔. 실제 컴포넌트들이 위치한 경로를 입력해야 합니다.
179
+ 7. 잘못된 파일이 열리거나 매칭이
180
+ `workspaceRoot`가 Angular 루트인지 확인
181
+ 8. 하이라이트가 보이거나 info가 null로 나옴
182
+ `http://localhost:${port}/__cmp-map` 에서 컴포넌트 정보가 나타나는지 확인
183
+ 9. 포트 충돌
180
184
  `ngx-locatorjs.config.json`과 `ngx-locatorjs.proxy.json`에서 포트 일치 여부 확인
181
185
 
182
186
  **주의**
183
187
  - 개발 모드에서만 사용하세요. 프로덕션 번들에 포함되지 않도록 `environment.production` 체크를 권장합니다.
188
+ - 네트워크 요청은 opt-in이며 localhost로만 제한됩니다. `enableNetwork: true`로 활성화하세요.
184
189
 
185
190
  **원 커맨드 실행 (추천)**
186
191
  file-opener 서버와 Angular dev server를 한 번에 띄우려면 아래 방식 중 하나를 사용하세요.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ngx-locatorjs (Angular Open-in-Editor)
2
2
 
3
+ ![2026-02-045 13 22-ezgif com-optimize](https://github.com/user-attachments/assets/9956e311-1af6-4096-9b9e-eb8dd6ea62be)
4
+
3
5
  한국어 문서: [README.ko.md](README.ko.md)
4
6
 
5
7
  Open Angular component files directly from the browser with **Alt + Click** during development. This package provides:
@@ -7,6 +9,8 @@ Open Angular component files directly from the browser with **Alt + Click** duri
7
9
  - CLI tools to scan Angular components and open files in your editor
8
10
  - Config + proxy setup guidance
9
11
 
12
+ Inspired by [locatorjs.com](https://www.locatorjs.com/).
13
+
10
14
  ## Features
11
15
  - **Alt + Click**: open template (.html)
12
16
  - **Alt + Shift + Click**: open component (.ts)
@@ -19,14 +23,14 @@ npm i -D ngx-locatorjs
19
23
  ```
20
24
 
21
25
  ## Required Steps (Do This First)
22
- You must complete steps 1–5 for this to work.
26
+ You must complete steps 1–4 for this to work.
23
27
 
24
28
  1. Install the package: `npm i -D ngx-locatorjs`
25
- 2. Add the runtime hook to `main.ts` (see the examples below)
26
- 3. Generate config + proxy: `npx locatorjs-config`
27
- 4. Scan components: `npx locatorjs-scan`
28
- 5. Run the file-opener server and your dev server (keep both running): `npx locatorjs-open-in-editor` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
29
- - If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
29
+ 2. Generate config + proxy: `npx locatorjs-config`
30
+ 3. Add the runtime hook to `main.ts` (see the examples below)
31
+ 4. Run the file-opener server and your dev server (keep both running): `npx locatorjs-open-in-editor --watch` + `ng serve --proxy-config ngx-locatorjs.proxy.json`
32
+
33
+ If you use `npm run start`, pass args after `--`: `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
30
34
 
31
35
  ## Add to `main.ts`
32
36
 
@@ -47,7 +51,9 @@ platformBrowserDynamic()
47
51
  if (!environment.production) {
48
52
  setTimeout(() => {
49
53
  import('ngx-locatorjs')
50
- .then((m) => m.installAngularLocator())
54
+ .then((m) =>
55
+ m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
56
+ )
51
57
  .catch((err) => console.warn('[angular-locator] Failed to load:', err));
52
58
  }, 1000);
53
59
  }
@@ -65,7 +71,9 @@ bootstrapApplication(AppComponent, appConfig)
65
71
  .then(() => {
66
72
  setTimeout(() => {
67
73
  import('ngx-locatorjs')
68
- .then((m) => m.installAngularLocator())
74
+ .then((m) =>
75
+ m.installAngularLocator({ enableNetwork: true }), // required for network access (localhost-only)
76
+ )
69
77
  .catch((err) => console.warn('[angular-locator] Failed to load:', err));
70
78
  }, 1000);
71
79
  })
@@ -77,24 +85,30 @@ Location: project root
77
85
 
78
86
  **Important**
79
87
  - `npx locatorjs-config` uses the **current directory** as the base.
80
- - Run it from the project root and press **Enter** for `workspaceRoot: "."`.
81
- - In a monorepo, enter the **relative path** to your Angular app (e.g. `apps/web`).
88
+ - Defaults: `port: 4123`, `workspaceRoot: "."`.
89
+ - In a monorepo, update `workspaceRoot` to the **relative path** of your Angular app (e.g. `apps/web`).
82
90
  - If `.gitignore` exists, `npx locatorjs-config` will append `.open-in-editor/`. Remove it if you want to commit the map.
83
91
 
84
92
  Example:
85
93
  ```json
86
94
  {
87
- "port": 4123,
88
- "workspaceRoot": ".",
89
- "editor": "cursor",
90
- "fallbackEditor": "code",
95
+ "port": 4123, // Port for the local file-opener server
96
+ "workspaceRoot": ".", // Angular workspace root
97
+ "editor": "cursor", // Editor to open files (`cursor`, `code`, `webstorm`)
98
+ "fallbackEditor": "code", // Fallback editor if the default fails
91
99
  "scan": {
100
+ /**
101
+ * Globs to include when scanning components
102
+ */
92
103
  "includeGlobs": [
93
104
  "src/**/*.{ts,tsx}",
94
105
  "projects/**/*.{ts,tsx}",
95
106
  "apps/**/*.{ts,tsx}",
96
107
  "libs/**/*.{ts,tsx}"
97
108
  ],
109
+ /**
110
+ * Globs to exclude from scanning
111
+ */
98
112
  "excludeGlobs": [
99
113
  "**/node_modules/**",
100
114
  "**/dist/**",
@@ -108,15 +122,7 @@ Example:
108
122
  }
109
123
  ```
110
124
 
111
- ### Field Reference
112
- - `port`: file-opener server port
113
- - `workspaceRoot`: actual Angular workspace root
114
- - `editor`: default editor (`cursor`, `code`, `webstorm`)
115
- - `fallbackEditor`: used if default fails
116
- - `scan.includeGlobs`: component scan targets
117
- - `scan.excludeGlobs`: scan excludes
118
-
119
- ### Recommended includeGlobs
125
+ ### Example includeGlobs
120
126
  - Simple app: `"src/app/**/*.ts"`
121
127
  - Angular workspace: `"projects/**/*.{ts,tsx}"`
122
128
  - Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
@@ -154,15 +160,17 @@ Example:
154
160
  ## Troubleshooting
155
161
  - **CORS / JSON parse error**: ensure dev server uses `--proxy-config ngx-locatorjs.proxy.json`
156
162
  - **npm run shows "Unknown cli config --proxy-config"**: use `npm run start -- --proxy-config ngx-locatorjs.proxy.json`
163
+ - **Network disabled**: pass `enableNetwork: true` to `installAngularLocator`
157
164
  - **component-map.json not found**: run `npx locatorjs-scan`
165
+ - **Component changes not reflected**: run `npx locatorjs-open-in-editor --watch` or re-run `npx locatorjs-scan`
158
166
  - **Map is empty or missing components**: check `scan.includeGlobs` and rerun the scan
159
167
  - **Wrong files open or nothing matches**: confirm `workspaceRoot` points to the actual Angular app root
160
- - **No highlight / info is null**: make sure `/__cmp-map` is loading and includes your component class name
161
- - **Editor not opening**: install editor CLI or set `EDITOR_CMD`
168
+ - **No highlight / info is null**: make sure `http://localhost:${port}/__cmp-map` is loading and includes your component class name
162
169
  - **Port conflict**: change port in both `ngx-locatorjs.config.json` and `ngx-locatorjs.proxy.json`
163
170
 
164
171
  ## Notes
165
172
  - Use only in development (guard with `environment.production`).
173
+ - Network requests are opt-in and limited to localhost. Set `enableNetwork: true` to activate.
166
174
 
167
175
  ## One-Command Dev (Recommended)
168
176
  Running the file-opener server and Angular dev server separately is tedious. You can wire them into a single script.
@@ -201,7 +209,4 @@ npm i -D npm-run-all
201
209
  - Works with single apps, Angular workspace, and Nx layouts
202
210
 
203
211
  ## Limitations
204
- - Requires proxy setup (`ngx-locatorjs.proxy.json`), otherwise requests will fail
205
- - Requires the file-opener server to be running
206
- - Line matching in dynamic/repeated templates is heuristic, not perfect
207
212
  - Not supported in SSR/SSG runtime (browser DOM only)
@@ -1,2 +1,2 @@
1
1
  import { installAngularLocator } from './index';
2
- installAngularLocator();
2
+ installAngularLocator({ enableNetwork: true });
@@ -14,6 +14,7 @@ export type AngularLocatorEndpoints = {
14
14
  };
15
15
  export type AngularLocatorOptions = {
16
16
  endpoints?: Partial<AngularLocatorEndpoints>;
17
+ enableNetwork?: boolean;
17
18
  prefetchMap?: boolean;
18
19
  enableHover?: boolean;
19
20
  enableClick?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AA+CF,iBAAe,SAAS,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB9D;AA2eD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,iBAa9E;AAED,wBAAsB,mBAAmB,kBASxC;AAED,wBAAsB,mBAAmB,kBAExC;AAED,wBAAgB,yBAAyB,YAExC;AAED,eAAO,MAAM,SAAS;;CAErB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/browser/index.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AA4EF,iBAAe,SAAS,CAAC,YAAY,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAwB9D;AA+eD,wBAAsB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,iBAa9E;AAED,wBAAsB,mBAAmB,kBASxC;AAED,wBAAsB,mBAAmB,kBAExC;AAED,wBAAgB,yBAAyB,YAExC;AAED,eAAO,MAAM,SAAS;;CAErB,CAAC"}
@@ -5,6 +5,7 @@ const DEFAULT_ENDPOINTS = {
5
5
  };
6
6
  const DEFAULT_OPTIONS = {
7
7
  endpoints: DEFAULT_ENDPOINTS,
8
+ enableNetwork: false,
8
9
  prefetchMap: true,
9
10
  enableHover: true,
10
11
  enableClick: true,
@@ -16,6 +17,30 @@ let OPTIONS = DEFAULT_OPTIONS;
16
17
  let INSTALLED = false;
17
18
  let CMP_MAP = null;
18
19
  let mapLoadPromise = null;
20
+ const LOCALHOST_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
21
+ function isLocalhostHost(hostname) {
22
+ if (LOCALHOST_HOSTS.has(hostname))
23
+ return true;
24
+ if (hostname.endsWith('.localhost'))
25
+ return true;
26
+ return false;
27
+ }
28
+ function assertNetworkAllowed(url) {
29
+ if (!OPTIONS.enableNetwork) {
30
+ if (OPTIONS.debug) {
31
+ console.warn('[angular-locator] Network is disabled. Set enableNetwork: true.');
32
+ }
33
+ throw new Error('Network disabled');
34
+ }
35
+ const resolved = new URL(url, window.location.href);
36
+ if (!isLocalhostHost(resolved.hostname)) {
37
+ if (OPTIONS.debug) {
38
+ console.warn('[angular-locator] Network is limited to localhost:', resolved.href);
39
+ }
40
+ throw new Error('Network restricted to localhost');
41
+ }
42
+ return resolved.toString();
43
+ }
19
44
  function normalizeMap(map) {
20
45
  if (!map.filePathsByClassName || Object.keys(map.filePathsByClassName).length === 0) {
21
46
  const rebuilt = {};
@@ -34,7 +59,7 @@ async function ensureMap(forceRefresh = false) {
34
59
  if (CMP_MAP && !forceRefresh)
35
60
  return CMP_MAP;
36
61
  const timestamp = Date.now();
37
- const res = await fetch(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`, {
62
+ const res = await fetch(assertNetworkAllowed(`${OPTIONS.endpoints.componentMap}?t=${timestamp}`), {
38
63
  cache: 'no-store',
39
64
  headers: {
40
65
  'Cache-Control': 'no-cache, no-store, must-revalidate',
@@ -173,7 +198,7 @@ function getNearestComponent(el) {
173
198
  };
174
199
  }
175
200
  async function openFile(absPath, line = 1, col = 1) {
176
- const url = `${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`;
201
+ const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditor}?file=${encodeURIComponent(absPath)}&line=${line}&col=${col}`);
177
202
  try {
178
203
  await fetch(url);
179
204
  }
@@ -184,7 +209,7 @@ async function openFile(absPath, line = 1, col = 1) {
184
209
  }
185
210
  }
186
211
  async function openFileWithSearch(absPath, searchTerms) {
187
- const url = `${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`;
212
+ const url = assertNetworkAllowed(`${OPTIONS.endpoints.openInEditorSearch}?file=${encodeURIComponent(absPath)}&search=${encodeURIComponent(JSON.stringify(searchTerms))}`);
188
213
  try {
189
214
  await fetch(url);
190
215
  }
@@ -32,9 +32,10 @@ else {
32
32
  }
33
33
  async function startSetup() {
34
34
  try {
35
+ logDefaults();
35
36
  const config = {
36
- port: await promptPort(),
37
- workspaceRoot: await promptWorkspaceRoot(),
37
+ port: 4123,
38
+ workspaceRoot: '.',
38
39
  editor: await selectEditor(),
39
40
  fallbackEditor: 'code',
40
41
  scan: await promptScanSettings(),
@@ -193,54 +194,10 @@ function ensureGitignoreEntries(entries) {
193
194
  fs.appendFileSync(gitignorePath, block);
194
195
  console.log(`🧹 Added to .gitignore: ${missing.join(', ')}`);
195
196
  }
196
- function promptPort() {
197
- const rl = readline.createInterface({
198
- input: process.stdin,
199
- output: process.stdout,
200
- });
201
- return new Promise((resolve) => {
202
- rl.question('🔌 Enter port number (press Enter for default: 4123): ', (answer) => {
203
- rl.close();
204
- const port = answer.trim();
205
- const portNum = port === '' ? 4123 : parseInt(port, 10) || 4123;
206
- console.log(` → Port: ${portNum}`);
207
- resolve(portNum);
208
- });
209
- });
210
- }
211
- function promptWorkspaceRoot() {
212
- const rl = readline.createInterface({
213
- input: process.stdin,
214
- output: process.stdout,
215
- });
216
- console.log(`\n📁 Current directory: ${process.cwd()}`);
217
- const askWorkspaceRoot = () => {
218
- return new Promise((resolve) => {
219
- rl.question('📁 Enter workspace root (press Enter for current directory "."): ', (answer) => {
220
- const workspaceRoot = answer.trim();
221
- const result = workspaceRoot === '' ? '.' : workspaceRoot;
222
- const resolvedPath = path.resolve(process.cwd(), result);
223
- if (!fs.existsSync(resolvedPath)) {
224
- console.log(` ❌ Path does not exist: ${resolvedPath}`);
225
- console.log(' Please try again...\n');
226
- askWorkspaceRoot().then(resolve);
227
- return;
228
- }
229
- const stat = fs.statSync(resolvedPath);
230
- if (!stat.isDirectory()) {
231
- console.log(` ❌ Path is not a directory: ${resolvedPath}`);
232
- console.log(' Please try again...\n');
233
- askWorkspaceRoot().then(resolve);
234
- return;
235
- }
236
- console.log(` → Workspace root: ${result}`);
237
- console.log(` → Resolved path: ${resolvedPath}`);
238
- rl.close();
239
- resolve(result);
240
- });
241
- });
242
- };
243
- return askWorkspaceRoot();
197
+ function logDefaults() {
198
+ console.log('⚙️ Defaults applied:');
199
+ console.log(' → Port: 4123');
200
+ console.log(' → Workspace root: .');
244
201
  }
245
202
  function selectEditor() {
246
203
  const availableEditors = [
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import express from 'express';
3
2
  import fs from 'fs';
4
3
  import path from 'path';
5
4
  import childProcess from 'child_process';
5
+ import http from 'http';
6
+ import { spawn } from 'child_process';
7
+ import { fileURLToPath } from 'url';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
6
10
  const root = process.cwd();
7
11
  const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
8
12
  const configPath = path.resolve(root, CONFIG_FILENAME);
@@ -12,8 +16,19 @@ if (!fs.existsSync(configPath)) {
12
16
  console.log('Or manually create the config file.');
13
17
  process.exit(1);
14
18
  }
19
+ const DEFAULT_INCLUDE_GLOBS = [
20
+ 'src/**/*.{ts,tsx}',
21
+ 'projects/**/*.{ts,tsx}',
22
+ 'apps/**/*.{ts,tsx}',
23
+ 'libs/**/*.{ts,tsx}',
24
+ ];
15
25
  const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
16
- const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfg.port || 4123);
26
+ const WATCH_ENABLED = process.argv.includes('--watch') || process.argv.includes('-w');
27
+ const cfgScan = cfg.scan ?? {};
28
+ const scanIncludeGlobs = cfgScan.includeGlobs ?? DEFAULT_INCLUDE_GLOBS;
29
+ const scanWorkspaceRoot = cfg.workspaceRoot?.trim() || '.';
30
+ const cfgPort = cfg.port;
31
+ const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfgPort || 4123);
17
32
  const MAP_PATH = path.resolve(root, '.open-in-editor/component-map.json');
18
33
  const editorCLICache = {};
19
34
  function checkEditorCLI(editorName, cliCommand = editorName) {
@@ -108,13 +123,6 @@ function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
108
123
  return true;
109
124
  return false;
110
125
  }
111
- const app = express();
112
- app.get('/__cmp-map', (req, res) => {
113
- if (!fs.existsSync(MAP_PATH))
114
- return res.status(404).send('component-map.json not found');
115
- res.setHeader('Content-Type', 'application/json');
116
- fs.createReadStream(MAP_PATH).pipe(res);
117
- });
118
126
  function findBestLineInFile(filePath, searchTerms) {
119
127
  try {
120
128
  const content = fs.readFileSync(filePath, 'utf8');
@@ -151,45 +159,178 @@ function findBestLineInFile(filePath, searchTerms) {
151
159
  return 1;
152
160
  }
153
161
  }
154
- app.get('/__open-in-editor', (req, res) => {
155
- let file = req.query.file;
156
- const line = req.query.line || '1';
157
- const col = req.query.col || '1';
158
- if (!file)
159
- return res.status(400).send('file is required');
160
- file = decodeURIComponent(file);
161
- console.log(`[file-opener] Opening file: ${file}:${line}:${col}`);
162
- const fileWithPos = `${file}:${line}:${col}`;
163
- const ok = launchInEditor(fileWithPos);
164
- if (!ok) {
165
- return res.status(500).send('Failed to launch editor. Check PATH or set EDITOR_CMD.');
166
- }
167
- res.end('ok');
168
- });
169
- app.get('/__open-in-editor-search', (req, res) => {
170
- let file = req.query.file;
171
- const searchParam = req.query.search;
172
- if (!file)
173
- return res.status(400).send('file is required');
174
- if (!searchParam)
175
- return res.status(400).send('search terms required');
176
- file = decodeURIComponent(file);
162
+ function startScanWatch() {
163
+ const scanScript = path.resolve(__dirname, 'cmp-scan.js');
164
+ if (!fs.existsSync(scanScript)) {
165
+ console.log('[file-opener] scan script not found, watch disabled.');
166
+ return;
167
+ }
168
+ const roots = getWatchRoots(scanIncludeGlobs, scanWorkspaceRoot);
169
+ if (roots.length === 0) {
170
+ console.log('[file-opener] watch roots not found, watch disabled.');
171
+ return;
172
+ }
173
+ const recursive = process.platform === 'darwin' || process.platform === 'win32';
174
+ const watchers = [];
175
+ let scanRunning = false;
176
+ let scanQueued = false;
177
+ let timer = null;
178
+ const runScan = (reason) => {
179
+ if (scanRunning) {
180
+ scanQueued = true;
181
+ return;
182
+ }
183
+ scanRunning = true;
184
+ const label = reason ? ` (${reason})` : '';
185
+ console.log(`[file-opener] scan started${label}`);
186
+ const scanProcess = spawn(process.execPath, [scanScript], {
187
+ stdio: 'inherit',
188
+ cwd: root,
189
+ });
190
+ scanProcess.on('close', (code) => {
191
+ scanRunning = false;
192
+ if (code === 0) {
193
+ console.log('[file-opener] scan completed');
194
+ }
195
+ else {
196
+ console.log('[file-opener] scan failed');
197
+ }
198
+ if (scanQueued) {
199
+ scanQueued = false;
200
+ scheduleScan('queued');
201
+ }
202
+ });
203
+ };
204
+ const scheduleScan = (reason) => {
205
+ if (timer)
206
+ clearTimeout(timer);
207
+ timer = setTimeout(() => runScan(reason), 500);
208
+ };
209
+ const attachWatcher = (watchPath) => {
210
+ try {
211
+ const watcher = fs.watch(watchPath, { recursive }, (eventType, filename) => {
212
+ const detail = filename ? `${eventType}:${filename.toString()}` : eventType;
213
+ scheduleScan(detail);
214
+ });
215
+ watchers.push(watcher);
216
+ }
217
+ catch (err) {
218
+ console.log(`[file-opener] failed to watch ${watchPath}: ${err?.message || err}`);
219
+ throw err;
220
+ }
221
+ };
177
222
  try {
178
- const searchTerms = JSON.parse(decodeURIComponent(searchParam));
179
- const bestLine = findBestLineInFile(file, searchTerms);
180
- const fileWithPos = `${file}:${bestLine}:1`;
223
+ roots.forEach(attachWatcher);
224
+ console.log(`[file-opener] watch enabled (${recursive ? 'recursive' : 'non-recursive'}): ${roots.join(', ')}`);
225
+ }
226
+ catch {
227
+ watchers.forEach((w) => w.close());
228
+ console.log('[file-opener] falling back to polling scan every 5s');
229
+ setInterval(() => runScan('poll'), 5000);
230
+ }
231
+ runScan('initial');
232
+ }
233
+ function getWatchRoots(includeGlobs, workspaceRoot) {
234
+ const roots = new Set();
235
+ includeGlobs.forEach((glob) => {
236
+ const base = globToBaseDir(glob);
237
+ const resolved = path.resolve(root, workspaceRoot, base);
238
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
239
+ roots.add(resolved);
240
+ }
241
+ });
242
+ return Array.from(roots);
243
+ }
244
+ function globToBaseDir(glob) {
245
+ const wildcardIndex = glob.search(/[*?[\]{]/);
246
+ if (wildcardIndex === -1)
247
+ return glob;
248
+ const prefix = glob.slice(0, wildcardIndex);
249
+ if (prefix.endsWith('/'))
250
+ return prefix.slice(0, -1);
251
+ return path.dirname(prefix);
252
+ }
253
+ const server = http.createServer((req, res) => {
254
+ if (!req.url) {
255
+ res.statusCode = 400;
256
+ res.end('Bad request');
257
+ return;
258
+ }
259
+ if (req.method !== 'GET') {
260
+ res.statusCode = 405;
261
+ res.end('Method not allowed');
262
+ return;
263
+ }
264
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
265
+ const pathname = url.pathname;
266
+ if (pathname === '/__cmp-map') {
267
+ if (!fs.existsSync(MAP_PATH)) {
268
+ res.statusCode = 404;
269
+ res.end('component-map.json not found');
270
+ return;
271
+ }
272
+ res.setHeader('Content-Type', 'application/json');
273
+ fs.createReadStream(MAP_PATH).pipe(res);
274
+ return;
275
+ }
276
+ if (pathname === '/__open-in-editor') {
277
+ const file = url.searchParams.get('file');
278
+ const line = url.searchParams.get('line') || '1';
279
+ const col = url.searchParams.get('col') || '1';
280
+ if (!file) {
281
+ res.statusCode = 400;
282
+ res.end('file is required');
283
+ return;
284
+ }
285
+ const decoded = decodeURIComponent(file);
286
+ console.log(`[file-opener] Opening file: ${decoded}:${line}:${col}`);
287
+ const fileWithPos = `${decoded}:${line}:${col}`;
181
288
  const ok = launchInEditor(fileWithPos);
182
289
  if (!ok) {
183
- return res.status(500).send('Failed to launch editor');
290
+ res.statusCode = 500;
291
+ res.end('Failed to launch editor. Check PATH or set EDITOR_CMD.');
292
+ return;
184
293
  }
185
- res.end(`Opened at line ${bestLine}`);
294
+ res.end('ok');
295
+ return;
186
296
  }
187
- catch (e) {
188
- console.warn(`[file-opener] Search error: ${e.message}`);
189
- res.status(500).send('Search failed: ' + e.message);
297
+ if (pathname === '/__open-in-editor-search') {
298
+ const file = url.searchParams.get('file');
299
+ const searchParam = url.searchParams.get('search');
300
+ if (!file) {
301
+ res.statusCode = 400;
302
+ res.end('file is required');
303
+ return;
304
+ }
305
+ if (!searchParam) {
306
+ res.statusCode = 400;
307
+ res.end('search terms required');
308
+ return;
309
+ }
310
+ const decoded = decodeURIComponent(file);
311
+ try {
312
+ const searchTerms = JSON.parse(decodeURIComponent(searchParam));
313
+ const bestLine = findBestLineInFile(decoded, searchTerms);
314
+ const fileWithPos = `${decoded}:${bestLine}:1`;
315
+ const ok = launchInEditor(fileWithPos);
316
+ if (!ok) {
317
+ res.statusCode = 500;
318
+ res.end('Failed to launch editor');
319
+ return;
320
+ }
321
+ res.end(`Opened at line ${bestLine}`);
322
+ }
323
+ catch (e) {
324
+ console.warn(`[file-opener] Search error: ${e.message}`);
325
+ res.statusCode = 500;
326
+ res.end('Search failed: ' + e.message);
327
+ }
328
+ return;
190
329
  }
330
+ res.statusCode = 404;
331
+ res.end('Not found');
191
332
  });
192
- app
333
+ server
193
334
  .listen(PORT, () => {
194
335
  console.log(`[file-opener] http://localhost:${PORT}`);
195
336
  console.log(` - map: ${path.relative(root, MAP_PATH)}`);
@@ -201,6 +342,9 @@ app
201
342
  console.log(` • ${editor.name}${precision}`);
202
343
  });
203
344
  }
345
+ if (WATCH_ENABLED) {
346
+ startScanWatch();
347
+ }
204
348
  })
205
349
  .on('error', (err) => {
206
350
  if (err.code === 'EADDRINUSE') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-locatorjs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "LocatorJs open-in-editor tools for Angular projects",
5
5
  "author": "antepost24",
6
6
  "type": "module",
@@ -54,13 +54,11 @@
54
54
  "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
55
55
  },
56
56
  "dependencies": {
57
- "express": "^4.18.2",
58
57
  "ts-morph": "^27.0.0"
59
58
  },
60
59
  "devDependencies": {
61
60
  "@typescript-eslint/eslint-plugin": "^7.18.0",
62
61
  "@typescript-eslint/parser": "^7.18.0",
63
- "@types/express": "^4.17.17",
64
62
  "@types/node": "^18.18.0",
65
63
  "eslint": "^8.57.1",
66
64
  "eslint-config-prettier": "^9.1.0",