ngx-locatorjs 0.3.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.
@@ -3,12 +3,27 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import readline from 'readline';
5
5
  import { spawn } from 'child_process';
6
+ import crypto from 'crypto';
6
7
  import { fileURLToPath } from 'url';
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const root = process.cwd();
10
11
  const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
11
- const PROXY_FILENAME = 'ngx-locatorjs.proxy.json';
12
+ const PROXY_FILENAME = 'ngx-locatorjs.proxy.cjs';
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
+ ]);
12
27
  const configPath = path.resolve(root, CONFIG_FILENAME);
13
28
  const proxyConfigPath = resolveProxyConfigPath();
14
29
  console.log('šŸš€ LocatorJs (Open-in-Editor) Configuration Setup\n');
@@ -33,36 +48,22 @@ else {
33
48
  async function startSetup() {
34
49
  try {
35
50
  logDefaults();
51
+ const authToken = generateAuthToken();
36
52
  const config = {
37
53
  port: 4123,
54
+ host: '127.0.0.1',
38
55
  workspaceRoot: '.',
39
56
  editor: await selectEditor(),
40
57
  fallbackEditor: 'code',
58
+ authToken,
41
59
  scan: await promptScanSettings(),
42
60
  };
43
61
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
44
- const proxyConfig = {
45
- '/__open-in-editor': {
46
- target: `http://localhost:${config.port}`,
47
- secure: false,
48
- changeOrigin: true,
49
- },
50
- '/__open-in-editor-search': {
51
- target: `http://localhost:${config.port}`,
52
- secure: false,
53
- changeOrigin: true,
54
- },
55
- '/__cmp-map': {
56
- target: `http://localhost:${config.port}`,
57
- secure: false,
58
- changeOrigin: true,
59
- },
60
- };
61
- const mergedProxyConfig = mergeProxyConfig(proxyConfigPath, proxyConfig);
62
- fs.writeFileSync(proxyConfigPath, JSON.stringify(mergedProxyConfig, null, 2));
62
+ fs.writeFileSync(proxyConfigPath, buildDynamicProxyModule());
63
63
  console.log('\nāœ… Configuration saved successfully!');
64
64
  console.log(`šŸ“ Config: ${path.relative(root, configPath)}`);
65
- console.log(`šŸ”— Proxy: ${path.relative(root, proxyConfigPath)} (port: ${config.port})`);
65
+ console.log(`šŸ”— Proxy: ${path.relative(root, proxyConfigPath)} (dynamic from config port)`);
66
+ console.log('šŸ”’ Request auth token generated and wired into proxy headers');
66
67
  ensureGitignoreEntries(['.open-in-editor/']);
67
68
  console.log('\nšŸ” Running component scan...');
68
69
  const scanScript = path.resolve(__dirname, 'cmp-scan.js');
@@ -108,45 +109,65 @@ function printNextSteps(proxyPath) {
108
109
  console.log(' npx locatorjs-open-in-editor');
109
110
  console.log(` (run your Angular dev server with --proxy-config ${path.relative(root, proxyPath)})`);
110
111
  }
111
- function mergeProxyConfig(proxyConfigPath, addition) {
112
- const existing = readProxyConfig(proxyConfigPath);
113
- if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
114
- return {
115
- ...existing,
116
- ...addition,
117
- };
118
- }
119
- if (fs.existsSync(proxyConfigPath)) {
120
- console.log('āš ļø Existing proxy config is not valid JSON. Overwriting with locator config.');
121
- }
122
- return addition;
112
+ function buildDynamicProxyModule() {
113
+ return `'use strict';
114
+
115
+ const fs = require('fs');
116
+ const path = require('path');
117
+
118
+ function loadLocatorConfig() {
119
+ const configPath = path.resolve(process.cwd(), '${CONFIG_FILENAME}');
120
+ try {
121
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
122
+ } catch {
123
+ return {};
124
+ }
123
125
  }
124
- function readProxyConfig(filePath) {
125
- if (!fs.existsSync(filePath))
126
- return null;
127
- try {
128
- const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
129
- if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
130
- return existing;
131
- }
132
- }
133
- catch {
134
- // ignore parse errors
135
- }
136
- return null;
126
+
127
+ const cfg = loadLocatorConfig();
128
+ const host = typeof cfg.host === 'string' && cfg.host.trim() ? cfg.host.trim() : '127.0.0.1';
129
+ const port = Number(process.env.OPEN_IN_EDITOR_PORT || cfg.port || 4123);
130
+ const authToken = typeof cfg.authToken === 'string' ? cfg.authToken : '';
131
+ const headers = authToken ? { 'x-locatorjs-token': authToken } : {};
132
+ const target = \`http://\${host}:\${port}\`;
133
+
134
+ module.exports = {
135
+ '/__open-in-editor': {
136
+ target,
137
+ secure: false,
138
+ changeOrigin: true,
139
+ headers,
140
+ },
141
+ '/__open-in-editor-search': {
142
+ target,
143
+ secure: false,
144
+ changeOrigin: true,
145
+ headers,
146
+ },
147
+ '/__cmp-map': {
148
+ target,
149
+ secure: false,
150
+ changeOrigin: true,
151
+ headers,
152
+ },
153
+ };
154
+ `;
137
155
  }
138
156
  function resolveProxyConfigPath() {
139
157
  const angularProxyPath = findProxyConfigFromAngularJson();
140
158
  if (angularProxyPath) {
141
- if (path.extname(angularProxyPath) !== '.json') {
142
- console.log(`āš ļø proxyConfig in angular.json is not a JSON file (${path.basename(angularProxyPath)}). Creating ${PROXY_FILENAME} instead.`);
159
+ const ext = path.extname(angularProxyPath).toLowerCase();
160
+ if (ext === '.js' || ext === '.cjs') {
161
+ return angularProxyPath;
162
+ }
163
+ if (ext === '.json') {
164
+ console.log(`āš ļø angular.json points to JSON proxy (${path.basename(angularProxyPath)}).`);
165
+ console.log(` Creating dynamic proxy module ${PROXY_FILENAME} instead. Update angular.json proxyConfig to use it for single-source port management.`);
143
166
  return path.resolve(root, PROXY_FILENAME);
144
167
  }
145
- return angularProxyPath;
168
+ console.log(`āš ļø Unsupported proxy extension (${path.basename(angularProxyPath)}). Creating ${PROXY_FILENAME} instead.`);
169
+ return path.resolve(root, PROXY_FILENAME);
146
170
  }
147
- const defaultProxy = path.resolve(root, 'proxy.conf.json');
148
- if (fs.existsSync(defaultProxy))
149
- return defaultProxy;
150
171
  return path.resolve(root, PROXY_FILENAME);
151
172
  }
152
173
  function findProxyConfigFromAngularJson() {
@@ -198,8 +219,12 @@ function ensureGitignoreEntries(entries) {
198
219
  function logDefaults() {
199
220
  console.log('āš™ļø Defaults applied:');
200
221
  console.log(' → Port: 4123');
222
+ console.log(' → Host: 127.0.0.1');
201
223
  console.log(' → Workspace root: .');
202
224
  }
225
+ function generateAuthToken() {
226
+ return crypto.randomBytes(24).toString('hex');
227
+ }
203
228
  function selectEditor() {
204
229
  const availableEditors = [
205
230
  { name: 'Cursor', value: 'cursor' },
@@ -276,28 +301,280 @@ function selectEditor() {
276
301
  process.stdin.on('data', handleKeypress);
277
302
  });
278
303
  }
279
- function promptScanSettings() {
304
+ async function promptScanSettings() {
280
305
  const defaultInclude = [
281
- 'src/**/*.{ts,tsx}',
282
- 'projects/**/*.{ts,tsx}',
283
- 'apps/**/*.{ts,tsx}',
284
- '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}',
285
310
  ];
286
311
  const defaultExclude = [
287
312
  '**/node_modules/**',
288
313
  '**/dist/**',
289
314
  '**/.angular/**',
290
315
  '**/coverage/**',
291
- '**/*.spec.ts',
292
- '**/*.test.ts',
293
- '**/*.e2e.ts',
316
+ '**/*.spec.{ts,tsx,js,jsx}',
317
+ '**/*.test.{ts,tsx,js,jsx}',
318
+ '**/*.e2e.{ts,tsx,js,jsx}',
294
319
  ];
295
- console.log('\nšŸ“‚ Scan settings (using defaults):');
296
- console.log(` → Include: ${JSON.stringify(defaultInclude)}`);
297
- console.log(` → Exclude: ${JSON.stringify(defaultExclude)}`);
298
- console.log(` šŸ’” You can modify these later in ${CONFIG_FILENAME}`);
299
- return Promise.resolve({
300
- includeGlobs: defaultInclude,
301
- 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}`);
565
+ });
566
+ console.log(`\n${bold(yellow('Exclude globs'))}:`);
567
+ excludeGlobs.values.forEach((value) => {
568
+ console.log(` ${yellow('•')} ${value}`);
302
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}`)}`);
303
580
  }