ngx-locatorjs 0.4.0 → 0.5.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,7 +1,7 @@
1
1
  # ngx-locatorjs (Open-in-Editor)
2
2
 
3
+ Angular 인기가 적어서인지 [LocatorJs](https://www.locatorjs.com/)와 같은 개발 도구가 없어 불편했습니다. 그래서 CODEX와 함께 만들어보았습니다.
3
4
  브라우저에서 Alt+클릭으로 Angular 컴포넌트 파일을 에디터에서 바로 여는 개발용 도구입니다. Angular 프로젝트 어디에나 npm 패키지로 설치해 사용할 수 있습니다.
4
- 이 프로젝트는 [locatorjs.com](https://www.locatorjs.com/)에서 영감을 받았습니다.
5
5
 
6
6
  **기능**
7
7
 
@@ -109,19 +109,19 @@ bootstrapApplication(AppComponent, appConfig)
109
109
  "authToken": "locatorjs-config가 자동 생성",
110
110
  "scan": {
111
111
  "includeGlobs": [
112
- "src/**/*.{ts,tsx}",
113
- "projects/**/*.{ts,tsx}",
114
- "apps/**/*.{ts,tsx}",
115
- "libs/**/*.{ts,tsx}"
112
+ "src/**/*.{ts,tsx,js,jsx}",
113
+ "projects/**/*.{ts,tsx,js,jsx}",
114
+ "apps/**/*.{ts,tsx,js,jsx}",
115
+ "libs/**/*.{ts,tsx,js,jsx}"
116
116
  ],
117
117
  "excludeGlobs": [
118
118
  "**/node_modules/**",
119
119
  "**/dist/**",
120
120
  "**/.angular/**",
121
121
  "**/coverage/**",
122
- "**/*.spec.ts",
123
- "**/*.test.ts",
124
- "**/*.e2e.ts"
122
+ "**/*.spec.{ts,tsx,js,jsx}",
123
+ "**/*.test.{ts,tsx,js,jsx}",
124
+ "**/*.e2e.{ts,tsx,js,jsx}"
125
125
  ]
126
126
  }
127
127
  }
@@ -138,14 +138,27 @@ bootstrapApplication(AppComponent, appConfig)
138
138
  - `scan.includeGlobs`: 컴포넌트 소스 파일 탐색 대상 glob 목록입니다.
139
139
  - `scan.excludeGlobs`: 컴포넌트 스캔에서 제외할 glob 목록입니다.
140
140
 
141
+ **authToken 동작 방식 (0.5.0)**
142
+
143
+ - `locatorjs-config`가 프로젝트별 `authToken`을 `ngx-locatorjs.config.json`에 자동 생성합니다.
144
+ - `ngx-locatorjs.proxy.cjs`가 해당 토큰을 읽어 opener 라우트로 `x-locatorjs-token` 헤더를 전달합니다.
145
+ - Angular proxy 없이 opener 엔드포인트를 직접 호출할 경우 `installAngularLocator(...)`에 `authToken`을 전달해야 인증됩니다.
146
+ - `401 Unauthorized`가 발생하면 `npx locatorjs-config`를 다시 실행해 config/proxy를 재생성하세요.
147
+
148
+ **자동 스캔 범위 탐색 (0.5.0)**
149
+
150
+ - `locatorjs-config`가 스캔 루트를 자동으로 구성합니다.
151
+ - 기본 탐색 확장자는 `ts/tsx/js/jsx`를 포함합니다.
152
+ - 생성된 `scan.includeGlobs` / `scan.excludeGlobs`는 언제든 수동 수정 가능합니다.
153
+
141
154
  **프로젝트 구조별 includeGlobs 예시**
142
155
 
143
156
  1. 일반 Angular 앱
144
157
  `["src/app/**/*.ts"]`
145
158
  2. Angular Workspace (projects/)
146
- `["projects/**/*.{ts,tsx}"]`
159
+ `["projects/**/*.{ts,tsx,js,jsx}"]`
147
160
  3. Nx (apps/libs)
148
- `["apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"]`
161
+ `["apps/**/*.{ts,tsx,js,jsx}", "libs/**/*.{ts,tsx,js,jsx}"]`
149
162
 
150
163
  **환경변수 우선순위**
151
164
 
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  한국어 문서: [README.ko.md](README.ko.md)
6
6
 
7
+ Angular doesn’t have many developer tools like LocatorJs, likely due to lower popularity, which has been inconvenient. So I built one together with CODEX.
7
8
  Open Angular component files directly from the browser with **Alt + Click** during development. This package provides:
8
9
 
9
10
  - Browser runtime for Alt+click / hover UI
@@ -109,19 +110,19 @@ Example:
109
110
  "authToken": "generated-by-locatorjs-config",
110
111
  "scan": {
111
112
  "includeGlobs": [
112
- "src/**/*.{ts,tsx}",
113
- "projects/**/*.{ts,tsx}",
114
- "apps/**/*.{ts,tsx}",
115
- "libs/**/*.{ts,tsx}"
113
+ "src/**/*.{ts,tsx,js,jsx}",
114
+ "projects/**/*.{ts,tsx,js,jsx}",
115
+ "apps/**/*.{ts,tsx,js,jsx}",
116
+ "libs/**/*.{ts,tsx,js,jsx}"
116
117
  ],
117
118
  "excludeGlobs": [
118
119
  "**/node_modules/**",
119
120
  "**/dist/**",
120
121
  "**/.angular/**",
121
122
  "**/coverage/**",
122
- "**/*.spec.ts",
123
- "**/*.test.ts",
124
- "**/*.e2e.ts"
123
+ "**/*.spec.{ts,tsx,js,jsx}",
124
+ "**/*.test.{ts,tsx,js,jsx}",
125
+ "**/*.e2e.{ts,tsx,js,jsx}"
125
126
  ]
126
127
  }
127
128
  }
@@ -138,11 +139,24 @@ Example:
138
139
  - `scan.includeGlobs`: Globs used to find component source files.
139
140
  - `scan.excludeGlobs`: Globs excluded from component scanning.
140
141
 
142
+ ### authToken Behavior (0.5.0)
143
+
144
+ - `locatorjs-config` generates a per-project `authToken` in `ngx-locatorjs.config.json`.
145
+ - `ngx-locatorjs.proxy.cjs` reads that token and forwards it as `x-locatorjs-token` to opener routes.
146
+ - If you skip Angular proxy and call opener endpoints directly, pass `authToken` to `installAngularLocator(...)` so requests are authorized.
147
+ - If token mismatch occurs (`401 Unauthorized`), regenerate config/proxy: `npx locatorjs-config`.
148
+
149
+ ### Auto Scan Scope Discovery (0.5.0)
150
+
151
+ - `locatorjs-config` now auto-detects scan roots.
152
+ - Generated defaults now include `ts/tsx/js/jsx`.
153
+ - You can always edit the generated `scan.includeGlobs` / `scan.excludeGlobs` manually.
154
+
141
155
  ### Example includeGlobs
142
156
 
143
157
  - Simple app: `"src/app/**/*.ts"`
144
- - Angular workspace: `"projects/**/*.{ts,tsx}"`
145
- - Nx: `"apps/**/*.{ts,tsx}", "libs/**/*.{ts,tsx}"`
158
+ - Angular workspace: `"projects/**/*.{ts,tsx,js,jsx}"`
159
+ - Nx: `"apps/**/*.{ts,tsx,js,jsx}", "libs/**/*.{ts,tsx,js,jsx}"`
146
160
 
147
161
  ## Proxy (`ngx-locatorjs.proxy.cjs`)
148
162
 
@@ -4,19 +4,19 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { glob } from 'glob';
6
6
  const DEFAULT_INCLUDE_GLOBS = [
7
- 'src/**/*.{ts,tsx}',
8
- 'projects/**/*.{ts,tsx}',
9
- 'apps/**/*.{ts,tsx}',
10
- 'libs/**/*.{ts,tsx}',
7
+ 'src/**/*.{ts,tsx,js,jsx}',
8
+ 'projects/**/*.{ts,tsx,js,jsx}',
9
+ 'apps/**/*.{ts,tsx,js,jsx}',
10
+ 'libs/**/*.{ts,tsx,js,jsx}',
11
11
  ];
12
12
  const DEFAULT_EXCLUDE_GLOBS = [
13
13
  '**/node_modules/**',
14
14
  '**/dist/**',
15
15
  '**/.angular/**',
16
16
  '**/coverage/**',
17
- '**/*.spec.ts',
18
- '**/*.test.ts',
19
- '**/*.e2e.ts',
17
+ '**/*.spec.{ts,tsx,js,jsx}',
18
+ '**/*.test.{ts,tsx,js,jsx}',
19
+ '**/*.e2e.{ts,tsx,js,jsx}',
20
20
  ];
21
21
  const root = process.cwd();
22
22
  const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
@@ -10,6 +10,20 @@ const __dirname = path.dirname(__filename);
10
10
  const root = process.cwd();
11
11
  const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
12
12
  const PROXY_FILENAME = 'ngx-locatorjs.proxy.cjs';
13
+ const ANSI_ENABLED = Boolean(process.stdout.isTTY);
14
+ const IGNORED_SCAN_DIR_NAMES = new Set([
15
+ 'node_modules',
16
+ 'dist',
17
+ 'coverage',
18
+ '.angular',
19
+ '.git',
20
+ '.nx',
21
+ '.turbo',
22
+ '.cache',
23
+ 'build',
24
+ 'out',
25
+ 'tmp',
26
+ ]);
13
27
  const configPath = path.resolve(root, CONFIG_FILENAME);
14
28
  const proxyConfigPath = resolveProxyConfigPath();
15
29
  console.log('🚀 LocatorJs (Open-in-Editor) Configuration Setup\n');
@@ -287,28 +301,280 @@ function selectEditor() {
287
301
  process.stdin.on('data', handleKeypress);
288
302
  });
289
303
  }
290
- function promptScanSettings() {
304
+ async function promptScanSettings() {
291
305
  const defaultInclude = [
292
- 'src/**/*.{ts,tsx}',
293
- 'projects/**/*.{ts,tsx}',
294
- 'apps/**/*.{ts,tsx}',
295
- 'libs/**/*.{ts,tsx}',
306
+ 'src/**/*.{ts,tsx,js,jsx}',
307
+ 'projects/**/*.{ts,tsx,js,jsx}',
308
+ 'apps/**/*.{ts,tsx,js,jsx}',
309
+ 'libs/**/*.{ts,tsx,js,jsx}',
296
310
  ];
297
311
  const defaultExclude = [
298
312
  '**/node_modules/**',
299
313
  '**/dist/**',
300
314
  '**/.angular/**',
301
315
  '**/coverage/**',
302
- '**/*.spec.ts',
303
- '**/*.test.ts',
304
- '**/*.e2e.ts',
316
+ '**/*.spec.{ts,tsx,js,jsx}',
317
+ '**/*.test.{ts,tsx,js,jsx}',
318
+ '**/*.e2e.{ts,tsx,js,jsx}',
305
319
  ];
306
- console.log('\n📂 Scan settings (using defaults):');
307
- console.log(` → Include: ${JSON.stringify(defaultInclude)}`);
308
- console.log(` → Exclude: ${JSON.stringify(defaultExclude)}`);
309
- console.log(` 💡 You can modify these later in ${CONFIG_FILENAME}`);
310
- return Promise.resolve({
311
- includeGlobs: defaultInclude,
312
- excludeGlobs: defaultExclude,
320
+ const includeGlobs = detectIncludeGlobs(defaultInclude);
321
+ const excludeGlobs = detectExcludeGlobs(defaultExclude);
322
+ printScanSettingsSummary(includeGlobs, excludeGlobs);
323
+ return {
324
+ includeGlobs: includeGlobs.values,
325
+ excludeGlobs: excludeGlobs.values,
326
+ };
327
+ }
328
+ function detectIncludeGlobs(defaultInclude) {
329
+ const sources = [];
330
+ let detectedBaseDirs = detectIncludeBaseDirsFromAngularJson();
331
+ if (detectedBaseDirs.size > 0) {
332
+ sources.push('angular.json');
333
+ const supplemental = detectSupplementalWorkspaceBaseDirs(detectedBaseDirs);
334
+ if (supplemental.size > 0) {
335
+ supplemental.forEach((dir) => detectedBaseDirs.add(dir));
336
+ sources.push('workspace directories (not declared in angular.json)');
337
+ }
338
+ }
339
+ else {
340
+ detectedBaseDirs = detectIncludeBaseDirsFromConventionalLayout();
341
+ if (detectedBaseDirs.size > 0) {
342
+ sources.push('existing directories');
343
+ }
344
+ }
345
+ if (detectedBaseDirs.size > 0) {
346
+ return {
347
+ values: toGlobPatterns(detectedBaseDirs),
348
+ autoDetected: true,
349
+ sources,
350
+ };
351
+ }
352
+ return {
353
+ values: defaultInclude,
354
+ autoDetected: false,
355
+ sources: [],
356
+ };
357
+ }
358
+ function detectExcludeGlobs(defaultExclude) {
359
+ const extraExcludeCandidates = [
360
+ { pattern: '**/.nx/**', reason: 'nx.json', when: fileExists('nx.json') },
361
+ { pattern: '**/.turbo/**', reason: 'turbo.json', when: fileExists('turbo.json') },
362
+ { pattern: '**/.cache/**', reason: '.cache directory', when: directoryExists('.cache') },
363
+ { pattern: '**/build/**', reason: 'build directory', when: directoryExists('build') },
364
+ { pattern: '**/out/**', reason: 'out directory', when: directoryExists('out') },
365
+ { pattern: '**/tmp/**', reason: 'tmp directory', when: directoryExists('tmp') },
366
+ ];
367
+ const values = [...defaultExclude];
368
+ const reasons = [];
369
+ for (const candidate of extraExcludeCandidates) {
370
+ if (!candidate.when)
371
+ continue;
372
+ if (!values.includes(candidate.pattern)) {
373
+ values.push(candidate.pattern);
374
+ reasons.push(`${candidate.pattern} (${candidate.reason})`);
375
+ }
376
+ }
377
+ return {
378
+ values,
379
+ autoDetected: reasons.length > 0,
380
+ sources: reasons,
381
+ };
382
+ }
383
+ function detectIncludeBaseDirsFromAngularJson() {
384
+ const angularJsonPath = path.resolve(root, 'angular.json');
385
+ if (!fs.existsSync(angularJsonPath))
386
+ return new Set();
387
+ try {
388
+ const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
389
+ const projects = angularJson?.projects ?? {};
390
+ const detectedBaseDirs = new Set();
391
+ for (const project of Object.values(projects)) {
392
+ const sourceRoot = normalizeRelativePath(project.sourceRoot);
393
+ const projectRoot = normalizeRelativePath(project.root);
394
+ if (sourceRoot && directoryExists(sourceRoot)) {
395
+ detectedBaseDirs.add(sourceRoot);
396
+ continue;
397
+ }
398
+ if (!projectRoot)
399
+ continue;
400
+ const srcUnderProjectRoot = normalizeRelativePath(path.posix.join(projectRoot, 'src'));
401
+ if (srcUnderProjectRoot && directoryExists(srcUnderProjectRoot)) {
402
+ detectedBaseDirs.add(srcUnderProjectRoot);
403
+ }
404
+ else if (directoryExists(projectRoot)) {
405
+ detectedBaseDirs.add(projectRoot);
406
+ }
407
+ }
408
+ return detectedBaseDirs;
409
+ }
410
+ catch {
411
+ return new Set();
412
+ }
413
+ }
414
+ function detectIncludeBaseDirsFromConventionalLayout() {
415
+ const conventionalDirs = ['src', 'projects', 'apps', 'libs'];
416
+ const detectedBaseDirs = new Set();
417
+ for (const dir of conventionalDirs) {
418
+ if (directoryExists(dir)) {
419
+ detectedBaseDirs.add(dir);
420
+ }
421
+ }
422
+ return detectedBaseDirs;
423
+ }
424
+ function detectSupplementalWorkspaceBaseDirs(knownBaseDirs) {
425
+ const roots = ['projects', 'apps', 'libs'];
426
+ const supplemental = new Set();
427
+ for (const rootDir of roots) {
428
+ const rootPath = path.resolve(root, rootDir);
429
+ if (!directoryExists(rootDir))
430
+ continue;
431
+ let entries;
432
+ try {
433
+ entries = fs.readdirSync(rootPath, { withFileTypes: true });
434
+ }
435
+ catch {
436
+ continue;
437
+ }
438
+ for (const entry of entries) {
439
+ if (!entry.isDirectory())
440
+ continue;
441
+ const candidate = normalizeRelativePath(path.posix.join(rootDir, entry.name));
442
+ if (!candidate)
443
+ continue;
444
+ if (isCoveredByKnownBaseDirs(candidate, knownBaseDirs))
445
+ continue;
446
+ if (!directoryContainsScanCandidateFiles(candidate))
447
+ continue;
448
+ supplemental.add(candidate);
449
+ }
450
+ }
451
+ return supplemental;
452
+ }
453
+ function isCoveredByKnownBaseDirs(candidate, knownBaseDirs) {
454
+ for (const known of knownBaseDirs) {
455
+ if (candidate === known)
456
+ return true;
457
+ if (candidate.startsWith(`${known}/`))
458
+ return true;
459
+ if (known.startsWith(`${candidate}/`))
460
+ return true;
461
+ }
462
+ return false;
463
+ }
464
+ function directoryContainsScanCandidateFiles(relPath, maxDepth = 6) {
465
+ const absolute = path.resolve(root, relPath);
466
+ return directoryContainsScanCandidateFilesRecursive(absolute, 0, maxDepth);
467
+ }
468
+ function directoryContainsScanCandidateFilesRecursive(absolutePath, currentDepth, maxDepth) {
469
+ let entries;
470
+ try {
471
+ entries = fs.readdirSync(absolutePath, { withFileTypes: true });
472
+ }
473
+ catch {
474
+ return false;
475
+ }
476
+ for (const entry of entries) {
477
+ if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
478
+ return true;
479
+ }
480
+ if (!entry.isDirectory())
481
+ continue;
482
+ if (currentDepth >= maxDepth)
483
+ continue;
484
+ if (IGNORED_SCAN_DIR_NAMES.has(entry.name))
485
+ continue;
486
+ const childPath = path.join(absolutePath, entry.name);
487
+ if (directoryContainsScanCandidateFilesRecursive(childPath, currentDepth + 1, maxDepth)) {
488
+ return true;
489
+ }
490
+ }
491
+ return false;
492
+ }
493
+ function toGlobPatterns(baseDirs) {
494
+ if (baseDirs.size === 0)
495
+ return [];
496
+ const preferredOrder = ['src', 'projects', 'apps', 'libs'];
497
+ return Array.from(baseDirs)
498
+ .sort((a, b) => {
499
+ const aIdx = preferredOrder.indexOf(a);
500
+ const bIdx = preferredOrder.indexOf(b);
501
+ const aRank = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
502
+ const bRank = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
503
+ if (aRank !== bRank)
504
+ return aRank - bRank;
505
+ return a.localeCompare(b);
506
+ })
507
+ .map((baseDir) => `${baseDir}/**/*.{ts,tsx,js,jsx}`);
508
+ }
509
+ function normalizeRelativePath(value) {
510
+ if (!value)
511
+ return null;
512
+ const normalized = value
513
+ .trim()
514
+ .replace(/\\/g, '/')
515
+ .replace(/^\.\/+/, '')
516
+ .replace(/^\/+/, '')
517
+ .replace(/\/+$/, '');
518
+ return normalized.length > 0 ? normalized : null;
519
+ }
520
+ function directoryExists(relPath) {
521
+ try {
522
+ const stat = fs.statSync(path.resolve(root, relPath));
523
+ return stat.isDirectory();
524
+ }
525
+ catch {
526
+ return false;
527
+ }
528
+ }
529
+ function fileExists(relPath) {
530
+ try {
531
+ const stat = fs.statSync(path.resolve(root, relPath));
532
+ return stat.isFile();
533
+ }
534
+ catch {
535
+ return false;
536
+ }
537
+ }
538
+ function colorize(text, code) {
539
+ if (!ANSI_ENABLED)
540
+ return text;
541
+ return `\x1b[${code}m${text}\x1b[0m`;
542
+ }
543
+ function bold(text) {
544
+ return colorize(text, '1');
545
+ }
546
+ function cyan(text) {
547
+ return colorize(text, '36');
548
+ }
549
+ function green(text) {
550
+ return colorize(text, '32');
551
+ }
552
+ function yellow(text) {
553
+ return colorize(text, '33');
554
+ }
555
+ function dim(text) {
556
+ return colorize(text, '2');
557
+ }
558
+ function printScanSettingsSummary(includeGlobs, excludeGlobs) {
559
+ const mode = includeGlobs.autoDetected ? 'AUTO-DETECTED' : 'DEFAULTS';
560
+ console.log(`\n${bold(cyan('Scan Settings Applied'))} ${dim(`[${mode}]`)}`);
561
+ console.log(dim('────────────────────────────────────────────'));
562
+ console.log(`${bold(green('Include globs'))}:`);
563
+ includeGlobs.values.forEach((value) => {
564
+ console.log(` ${green('•')} ${value}`);
313
565
  });
566
+ console.log(`\n${bold(yellow('Exclude globs'))}:`);
567
+ excludeGlobs.values.forEach((value) => {
568
+ console.log(` ${yellow('•')} ${value}`);
569
+ });
570
+ if (includeGlobs.autoDetected) {
571
+ console.log(`\n${bold('Detected from')}: ${includeGlobs.sources.join(', ')}`);
572
+ }
573
+ if (excludeGlobs.autoDetected) {
574
+ console.log(`${bold('Auto-added excludes')}:`);
575
+ excludeGlobs.sources.forEach((value) => {
576
+ console.log(` ${yellow('•')} ${value}`);
577
+ });
578
+ }
579
+ console.log(`\n${dim(`Edit scan globs anytime in ${CONFIG_FILENAME}`)}`);
314
580
  }
@@ -21,10 +21,10 @@ function isErrnoException(error) {
21
21
  return typeof error === 'object' && error !== null && 'code' in error;
22
22
  }
23
23
  const DEFAULT_INCLUDE_GLOBS = [
24
- 'src/**/*.{ts,tsx}',
25
- 'projects/**/*.{ts,tsx}',
26
- 'apps/**/*.{ts,tsx}',
27
- 'libs/**/*.{ts,tsx}',
24
+ 'src/**/*.{ts,tsx,js,jsx}',
25
+ 'projects/**/*.{ts,tsx,js,jsx}',
26
+ 'apps/**/*.{ts,tsx,js,jsx}',
27
+ 'libs/**/*.{ts,tsx,js,jsx}',
28
28
  ];
29
29
  const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
30
30
  const WATCH_ENABLED = process.argv.includes('--watch') || process.argv.includes('-w');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-locatorjs",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "LocatorJs open-in-editor tools for Angular projects",
5
5
  "keywords": [
6
6
  "angular",