ngx-locatorjs 0.1.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/LICENSE +21 -0
- package/README.ko.md +214 -0
- package/README.md +207 -0
- package/dist/browser/auto.d.ts +2 -0
- package/dist/browser/auto.d.ts.map +1 -0
- package/dist/browser/auto.js +2 -0
- package/dist/browser/index.d.ts +33 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +528 -0
- package/dist/node/cmp-scan.js +158 -0
- package/dist/node/config-setup.js +342 -0
- package/dist/node/file-opener.js +214 -0
- package/package.json +70 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
|
|
11
|
+
const PROXY_FILENAME = 'ngx-locatorjs.proxy.json';
|
|
12
|
+
const configPath = path.resolve(root, CONFIG_FILENAME);
|
|
13
|
+
const proxyConfigPath = resolveProxyConfigPath();
|
|
14
|
+
console.log('đ LocatorJs (Open-in-Editor) Configuration Setup\n');
|
|
15
|
+
if (fs.existsSync(configPath)) {
|
|
16
|
+
console.log(`â ď¸ ${CONFIG_FILENAME} already exists!`);
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
});
|
|
21
|
+
rl.question('Do you want to overwrite it? (y/N): ', (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
24
|
+
console.log('Setup cancelled.');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
startSetup();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
startSetup();
|
|
32
|
+
}
|
|
33
|
+
async function startSetup() {
|
|
34
|
+
try {
|
|
35
|
+
const config = {
|
|
36
|
+
port: await promptPort(),
|
|
37
|
+
workspaceRoot: await promptWorkspaceRoot(),
|
|
38
|
+
editor: await selectEditor(),
|
|
39
|
+
fallbackEditor: 'code',
|
|
40
|
+
scan: await promptScanSettings(),
|
|
41
|
+
};
|
|
42
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
43
|
+
const proxyConfig = {
|
|
44
|
+
'/__open-in-editor': {
|
|
45
|
+
target: `http://localhost:${config.port}`,
|
|
46
|
+
secure: false,
|
|
47
|
+
changeOrigin: true,
|
|
48
|
+
},
|
|
49
|
+
'/__open-in-editor-search': {
|
|
50
|
+
target: `http://localhost:${config.port}`,
|
|
51
|
+
secure: false,
|
|
52
|
+
changeOrigin: true,
|
|
53
|
+
},
|
|
54
|
+
'/__cmp-map': {
|
|
55
|
+
target: `http://localhost:${config.port}`,
|
|
56
|
+
secure: false,
|
|
57
|
+
changeOrigin: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const mergedProxyConfig = mergeProxyConfig(proxyConfigPath, proxyConfig);
|
|
61
|
+
fs.writeFileSync(proxyConfigPath, JSON.stringify(mergedProxyConfig, null, 2));
|
|
62
|
+
console.log('\nâ
Configuration saved successfully!');
|
|
63
|
+
console.log(`đ Config: ${path.relative(root, configPath)}`);
|
|
64
|
+
console.log(`đ Proxy: ${path.relative(root, proxyConfigPath)} (port: ${config.port})`);
|
|
65
|
+
ensureGitignoreEntries(['.open-in-editor/']);
|
|
66
|
+
console.log('\nđ Running component scan...');
|
|
67
|
+
const scanScript = path.resolve(__dirname, 'cmp-scan.js');
|
|
68
|
+
if (fs.existsSync(scanScript)) {
|
|
69
|
+
try {
|
|
70
|
+
const scanProcess = spawn(process.execPath, [scanScript], {
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
cwd: root,
|
|
73
|
+
});
|
|
74
|
+
scanProcess.on('close', (code) => {
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
console.log('\nâ
Component scan completed!');
|
|
77
|
+
printNextSteps(proxyConfigPath);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log('\nâ ď¸ Component scan failed, but config is saved.');
|
|
81
|
+
printManualScan(proxyConfigPath);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
console.log('\nâ ď¸ Could not run component scan automatically.');
|
|
87
|
+
printManualScan(proxyConfigPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log('\nâ ď¸ scan script not found.');
|
|
92
|
+
printManualScan(proxyConfigPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error('\nâ Setup failed:', error?.message || error);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function printManualScan(proxyPath) {
|
|
101
|
+
console.log('Please run it manually: npx locatorjs-scan');
|
|
102
|
+
printNextSteps(proxyPath);
|
|
103
|
+
}
|
|
104
|
+
function printNextSteps(proxyPath) {
|
|
105
|
+
console.log('\nđ Next steps:');
|
|
106
|
+
console.log(' npx locatorjs-open-in-editor');
|
|
107
|
+
console.log(` (run your Angular dev server with --proxy-config ${path.relative(root, proxyPath)})`);
|
|
108
|
+
}
|
|
109
|
+
function mergeProxyConfig(proxyConfigPath, addition) {
|
|
110
|
+
const existing = readProxyConfig(proxyConfigPath);
|
|
111
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
112
|
+
return {
|
|
113
|
+
...existing,
|
|
114
|
+
...addition,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (fs.existsSync(proxyConfigPath)) {
|
|
118
|
+
console.log('â ď¸ Existing proxy config is not valid JSON. Overwriting with locator config.');
|
|
119
|
+
}
|
|
120
|
+
return addition;
|
|
121
|
+
}
|
|
122
|
+
function readProxyConfig(filePath) {
|
|
123
|
+
if (!fs.existsSync(filePath))
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
127
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
128
|
+
return existing;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore parse errors
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function resolveProxyConfigPath() {
|
|
137
|
+
const angularProxyPath = findProxyConfigFromAngularJson();
|
|
138
|
+
if (angularProxyPath) {
|
|
139
|
+
if (path.extname(angularProxyPath) !== '.json') {
|
|
140
|
+
console.log(`â ď¸ proxyConfig in angular.json is not a JSON file (${path.basename(angularProxyPath)}). Creating ${PROXY_FILENAME} instead.`);
|
|
141
|
+
return path.resolve(root, PROXY_FILENAME);
|
|
142
|
+
}
|
|
143
|
+
return angularProxyPath;
|
|
144
|
+
}
|
|
145
|
+
const defaultProxy = path.resolve(root, 'proxy.conf.json');
|
|
146
|
+
if (fs.existsSync(defaultProxy))
|
|
147
|
+
return defaultProxy;
|
|
148
|
+
return path.resolve(root, PROXY_FILENAME);
|
|
149
|
+
}
|
|
150
|
+
function findProxyConfigFromAngularJson() {
|
|
151
|
+
const angularJsonPath = path.resolve(root, 'angular.json');
|
|
152
|
+
if (!fs.existsSync(angularJsonPath))
|
|
153
|
+
return null;
|
|
154
|
+
try {
|
|
155
|
+
const angularJson = JSON.parse(fs.readFileSync(angularJsonPath, 'utf8'));
|
|
156
|
+
const projects = angularJson?.projects ?? {};
|
|
157
|
+
for (const project of Object.values(projects)) {
|
|
158
|
+
const targets = project?.architect || project?.targets;
|
|
159
|
+
const serve = targets?.serve;
|
|
160
|
+
if (!serve)
|
|
161
|
+
continue;
|
|
162
|
+
const direct = serve?.options?.proxyConfig;
|
|
163
|
+
if (typeof direct === 'string' && direct.trim().length > 0) {
|
|
164
|
+
return path.resolve(root, direct);
|
|
165
|
+
}
|
|
166
|
+
const configurations = serve?.configurations;
|
|
167
|
+
if (configurations && typeof configurations === 'object') {
|
|
168
|
+
for (const config of Object.values(configurations)) {
|
|
169
|
+
const confProxy = config?.proxyConfig;
|
|
170
|
+
if (typeof confProxy === 'string' && confProxy.trim().length > 0) {
|
|
171
|
+
return path.resolve(root, confProxy);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// ignore parse errors
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function ensureGitignoreEntries(entries) {
|
|
183
|
+
const gitignorePath = path.resolve(root, '.gitignore');
|
|
184
|
+
if (!fs.existsSync(gitignorePath))
|
|
185
|
+
return;
|
|
186
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
187
|
+
const lines = content.split(/\r?\n/);
|
|
188
|
+
const missing = entries.filter((entry) => !lines.includes(entry));
|
|
189
|
+
if (missing.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
const suffix = content.endsWith('\n') ? '' : '\n';
|
|
192
|
+
const block = `${suffix}# ngx-locatorjs\n${missing.join('\n')}\n`;
|
|
193
|
+
fs.appendFileSync(gitignorePath, block);
|
|
194
|
+
console.log(`đ§š Added to .gitignore: ${missing.join(', ')}`);
|
|
195
|
+
}
|
|
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();
|
|
244
|
+
}
|
|
245
|
+
function selectEditor() {
|
|
246
|
+
const availableEditors = [
|
|
247
|
+
{ name: 'Cursor', value: 'cursor' },
|
|
248
|
+
{ name: 'VS Code', value: 'code' },
|
|
249
|
+
{ name: 'WebStorm', value: 'webstorm' },
|
|
250
|
+
];
|
|
251
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
252
|
+
console.log('\nđŻ Please select your editor:');
|
|
253
|
+
availableEditors.forEach((editor, index) => {
|
|
254
|
+
console.log(` ${index + 1}. ${editor.name}`);
|
|
255
|
+
});
|
|
256
|
+
const rl = readline.createInterface({
|
|
257
|
+
input: process.stdin,
|
|
258
|
+
output: process.stdout,
|
|
259
|
+
});
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
rl.question('\nEnter number (1-3, default: 1 for Cursor): ', (answer) => {
|
|
262
|
+
rl.close();
|
|
263
|
+
const choice = parseInt(answer.trim(), 10) || 1;
|
|
264
|
+
const selected = availableEditors[Math.max(0, Math.min(choice - 1, availableEditors.length - 1))];
|
|
265
|
+
console.log(` â Selected: ${selected.name}`);
|
|
266
|
+
resolve(selected.value);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
let selectedIndex = 0;
|
|
271
|
+
const renderMenu = () => {
|
|
272
|
+
console.clear();
|
|
273
|
+
console.log('\nđŻ Please select your editor:');
|
|
274
|
+
console.log(' Use ââ to navigate, Enter to select (default: Cursor)\n');
|
|
275
|
+
availableEditors.forEach((editor, index) => {
|
|
276
|
+
const isSelected = index === selectedIndex;
|
|
277
|
+
const pointer = isSelected ? 'âś' : ' ';
|
|
278
|
+
const highlight = isSelected ? '\x1b[36m' : '';
|
|
279
|
+
const reset = isSelected ? '\x1b[0m' : '';
|
|
280
|
+
console.log(`${highlight}${pointer} ${editor.name}${reset}`);
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
process.stdin.setRawMode(true);
|
|
285
|
+
process.stdin.resume();
|
|
286
|
+
process.stdin.setEncoding('utf8');
|
|
287
|
+
renderMenu();
|
|
288
|
+
const handleKeypress = (key) => {
|
|
289
|
+
switch (key) {
|
|
290
|
+
case '\u001b[A':
|
|
291
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : availableEditors.length - 1;
|
|
292
|
+
renderMenu();
|
|
293
|
+
break;
|
|
294
|
+
case '\u001b[B':
|
|
295
|
+
selectedIndex = selectedIndex < availableEditors.length - 1 ? selectedIndex + 1 : 0;
|
|
296
|
+
renderMenu();
|
|
297
|
+
break;
|
|
298
|
+
case '\r':
|
|
299
|
+
case '\n':
|
|
300
|
+
process.stdin.setRawMode(false);
|
|
301
|
+
process.stdin.pause();
|
|
302
|
+
process.stdin.removeListener('data', handleKeypress);
|
|
303
|
+
const selected = availableEditors[selectedIndex];
|
|
304
|
+
console.log(`\n⨠Selected: ${selected.name}`);
|
|
305
|
+
resolve(selected.value);
|
|
306
|
+
break;
|
|
307
|
+
case '\u0003':
|
|
308
|
+
console.log('\n\nCancelled. Setting Cursor as default.');
|
|
309
|
+
process.stdin.setRawMode(false);
|
|
310
|
+
process.stdin.pause();
|
|
311
|
+
resolve('cursor');
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
process.stdin.on('data', handleKeypress);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function promptScanSettings() {
|
|
319
|
+
const defaultInclude = [
|
|
320
|
+
'src/**/*.{ts,tsx}',
|
|
321
|
+
'projects/**/*.{ts,tsx}',
|
|
322
|
+
'apps/**/*.{ts,tsx}',
|
|
323
|
+
'libs/**/*.{ts,tsx}',
|
|
324
|
+
];
|
|
325
|
+
const defaultExclude = [
|
|
326
|
+
'**/node_modules/**',
|
|
327
|
+
'**/dist/**',
|
|
328
|
+
'**/.angular/**',
|
|
329
|
+
'**/coverage/**',
|
|
330
|
+
'**/*.spec.ts',
|
|
331
|
+
'**/*.test.ts',
|
|
332
|
+
'**/*.e2e.ts',
|
|
333
|
+
];
|
|
334
|
+
console.log('\nđ Scan settings (using defaults):');
|
|
335
|
+
console.log(` â Include: ${JSON.stringify(defaultInclude)}`);
|
|
336
|
+
console.log(` â Exclude: ${JSON.stringify(defaultExclude)}`);
|
|
337
|
+
console.log(` đĄ You can modify these later in ${CONFIG_FILENAME}`);
|
|
338
|
+
return Promise.resolve({
|
|
339
|
+
includeGlobs: defaultInclude,
|
|
340
|
+
excludeGlobs: defaultExclude,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import childProcess from 'child_process';
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const CONFIG_FILENAME = 'ngx-locatorjs.config.json';
|
|
8
|
+
const configPath = path.resolve(root, CONFIG_FILENAME);
|
|
9
|
+
if (!fs.existsSync(configPath)) {
|
|
10
|
+
console.log(`đ ${CONFIG_FILENAME} not found!`);
|
|
11
|
+
console.log('Please run: npx locatorjs-config');
|
|
12
|
+
console.log('Or manually create the config file.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
16
|
+
const PORT = Number(process.env.OPEN_IN_EDITOR_PORT || cfg.port || 4123);
|
|
17
|
+
const MAP_PATH = path.resolve(root, '.open-in-editor/component-map.json');
|
|
18
|
+
const editorCLICache = {};
|
|
19
|
+
function checkEditorCLI(editorName, cliCommand = editorName) {
|
|
20
|
+
if (editorCLICache[editorName] !== undefined)
|
|
21
|
+
return editorCLICache[editorName];
|
|
22
|
+
try {
|
|
23
|
+
childProcess.execSync(`which ${cliCommand}`, { stdio: 'ignore' });
|
|
24
|
+
editorCLICache[editorName] = true;
|
|
25
|
+
console.log(`[file-opener] ${editorName} CLI found, using precise line navigation`);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
editorCLICache[editorName] = false;
|
|
29
|
+
console.log(`[file-opener] ${editorName} CLI not found, using fallback method`);
|
|
30
|
+
}
|
|
31
|
+
return editorCLICache[editorName];
|
|
32
|
+
}
|
|
33
|
+
const MAC_APP_NAMES = {
|
|
34
|
+
cursor: 'Cursor',
|
|
35
|
+
code: 'Visual Studio Code',
|
|
36
|
+
webstorm: 'WebStorm',
|
|
37
|
+
};
|
|
38
|
+
function detectAvailableEditors() {
|
|
39
|
+
const editors = ['cursor', 'code', 'webstorm'];
|
|
40
|
+
const available = [];
|
|
41
|
+
for (const editor of editors) {
|
|
42
|
+
if (checkEditorCLI(editor)) {
|
|
43
|
+
available.push({ name: editor, hasCliPrecision: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return available;
|
|
47
|
+
}
|
|
48
|
+
const AVAILABLE_EDITORS = detectAvailableEditors();
|
|
49
|
+
const DEFAULT_EDITOR = process.env.LAUNCH_EDITOR || cfg.editor || AVAILABLE_EDITORS[0]?.name || 'cursor';
|
|
50
|
+
const FALLBACK_EDITOR = cfg.fallbackEditor || AVAILABLE_EDITORS[1]?.name || 'code';
|
|
51
|
+
const COMMAND_TEMPLATES = {
|
|
52
|
+
cursor: (file) => {
|
|
53
|
+
if (checkEditorCLI('cursor')) {
|
|
54
|
+
return ['cursor', ['--goto', file]];
|
|
55
|
+
}
|
|
56
|
+
const filePath = file.split(':')[0];
|
|
57
|
+
return ['open', ['-a', MAC_APP_NAMES.cursor, filePath]];
|
|
58
|
+
},
|
|
59
|
+
code: (file) => {
|
|
60
|
+
if (checkEditorCLI('code')) {
|
|
61
|
+
return ['code', ['--goto', file]];
|
|
62
|
+
}
|
|
63
|
+
const filePath = file.split(':')[0];
|
|
64
|
+
return ['open', ['-a', MAC_APP_NAMES.code, filePath]];
|
|
65
|
+
},
|
|
66
|
+
webstorm: (file) => {
|
|
67
|
+
if (checkEditorCLI('webstorm')) {
|
|
68
|
+
const [filePath, line, col] = file.split(':');
|
|
69
|
+
const args = [filePath];
|
|
70
|
+
if (line)
|
|
71
|
+
args.push('--line', line);
|
|
72
|
+
if (col)
|
|
73
|
+
args.push('--column', col);
|
|
74
|
+
return ['webstorm', args];
|
|
75
|
+
}
|
|
76
|
+
const filePath = file.split(':')[0];
|
|
77
|
+
return ['open', ['-a', MAC_APP_NAMES.webstorm, filePath]];
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
function launchInEditor(fileWithPos, preferred = DEFAULT_EDITOR) {
|
|
81
|
+
if (process.env.EDITOR_CMD) {
|
|
82
|
+
const [cmd, ...rest] = process.env.EDITOR_CMD.split(' ');
|
|
83
|
+
try {
|
|
84
|
+
childProcess.spawn(cmd, [...rest, fileWithPos], { stdio: 'ignore', detached: true }).unref();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.log(`[file-opener] EDITOR_CMD failed: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const tryRun = (editor) => {
|
|
92
|
+
const mk = COMMAND_TEMPLATES[editor];
|
|
93
|
+
if (!mk)
|
|
94
|
+
return false;
|
|
95
|
+
const [cmd, args] = mk(fileWithPos);
|
|
96
|
+
try {
|
|
97
|
+
childProcess.spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
console.log(`[file-opener] ${editor} failed: ${e.message}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (tryRun(preferred))
|
|
106
|
+
return true;
|
|
107
|
+
if (FALLBACK_EDITOR && tryRun(FALLBACK_EDITOR))
|
|
108
|
+
return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
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
|
+
function findBestLineInFile(filePath, searchTerms) {
|
|
119
|
+
try {
|
|
120
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
121
|
+
const lines = content.split('\n');
|
|
122
|
+
const scores = new Array(lines.length).fill(0);
|
|
123
|
+
searchTerms.forEach((term, termIndex) => {
|
|
124
|
+
const weight = Math.max(1, searchTerms.length - termIndex);
|
|
125
|
+
lines.forEach((line, lineIndex) => {
|
|
126
|
+
const lowerLine = line.toLowerCase();
|
|
127
|
+
const lowerTerm = term.toLowerCase();
|
|
128
|
+
if (lowerLine.includes(lowerTerm)) {
|
|
129
|
+
scores[lineIndex] += weight * 2;
|
|
130
|
+
if (new RegExp(`\\b${lowerTerm.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\b`).test(lowerLine)) {
|
|
131
|
+
scores[lineIndex] += weight * 3;
|
|
132
|
+
}
|
|
133
|
+
if (lowerLine.trim().startsWith(lowerTerm)) {
|
|
134
|
+
scores[lineIndex] += weight * 1.5;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
let bestLine = 1;
|
|
140
|
+
let bestScore = 0;
|
|
141
|
+
scores.forEach((score, index) => {
|
|
142
|
+
if (score > bestScore) {
|
|
143
|
+
bestScore = score;
|
|
144
|
+
bestLine = index + 1;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return bestScore > 0 ? bestLine : 1;
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.warn(`[file-opener] Failed to search in file: ${e.message}`);
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
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);
|
|
177
|
+
try {
|
|
178
|
+
const searchTerms = JSON.parse(decodeURIComponent(searchParam));
|
|
179
|
+
const bestLine = findBestLineInFile(file, searchTerms);
|
|
180
|
+
const fileWithPos = `${file}:${bestLine}:1`;
|
|
181
|
+
const ok = launchInEditor(fileWithPos);
|
|
182
|
+
if (!ok) {
|
|
183
|
+
return res.status(500).send('Failed to launch editor');
|
|
184
|
+
}
|
|
185
|
+
res.end(`Opened at line ${bestLine}`);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
console.warn(`[file-opener] Search error: ${e.message}`);
|
|
189
|
+
res.status(500).send('Search failed: ' + e.message);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
app
|
|
193
|
+
.listen(PORT, () => {
|
|
194
|
+
console.log(`[file-opener] http://localhost:${PORT}`);
|
|
195
|
+
console.log(` - map: ${path.relative(root, MAP_PATH)}`);
|
|
196
|
+
console.log(` - editor: ${DEFAULT_EDITOR} (fallback: ${FALLBACK_EDITOR})`);
|
|
197
|
+
if (AVAILABLE_EDITORS.length > 0) {
|
|
198
|
+
console.log(' - detected editors:');
|
|
199
|
+
AVAILABLE_EDITORS.forEach((editor) => {
|
|
200
|
+
const precision = editor.hasCliPrecision ? ' (precise line navigation)' : ' (app only)';
|
|
201
|
+
console.log(` ⢠${editor.name}${precision}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.on('error', (err) => {
|
|
206
|
+
if (err.code === 'EADDRINUSE') {
|
|
207
|
+
console.log(`[file-opener] Port ${PORT} already in use - another file:opener is already running`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.error('[file-opener] Server error:', err);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ngx-locatorjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LocatorJs open-in-editor tools for Angular projects",
|
|
5
|
+
"author": "antepost24",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"angular",
|
|
10
|
+
"locatorjs",
|
|
11
|
+
"open-in-editor",
|
|
12
|
+
"devtools",
|
|
13
|
+
"component"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Ea-st-ring/ngx-locator.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/Ea-st-ring/ngx-locator/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/Ea-st-ring/ngx-locator#readme",
|
|
23
|
+
"main": "dist/browser/index.js",
|
|
24
|
+
"types": "dist/browser/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/browser/index.d.ts",
|
|
28
|
+
"import": "./dist/browser/index.js",
|
|
29
|
+
"default": "./dist/browser/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./auto": {
|
|
32
|
+
"types": "./dist/browser/auto.d.ts",
|
|
33
|
+
"import": "./dist/browser/auto.js",
|
|
34
|
+
"default": "./dist/browser/auto.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"locatorjs-open-in-editor": "dist/node/file-opener.js",
|
|
39
|
+
"locatorjs-config": "dist/node/config-setup.js",
|
|
40
|
+
"locatorjs-scan": "dist/node/cmp-scan.js"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "npm run build:browser && npm run build:node",
|
|
48
|
+
"build:browser": "tsc -p tsconfig.browser.json",
|
|
49
|
+
"build:node": "tsc -p tsconfig.node.json",
|
|
50
|
+
"clean": "rm -rf dist",
|
|
51
|
+
"prepare": "npm run build",
|
|
52
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
53
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
54
|
+
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\""
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"express": "^4.18.2",
|
|
58
|
+
"ts-morph": "^27.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
62
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
63
|
+
"@types/express": "^4.17.17",
|
|
64
|
+
"@types/node": "^18.18.0",
|
|
65
|
+
"eslint": "^8.57.1",
|
|
66
|
+
"eslint-config-prettier": "^9.1.0",
|
|
67
|
+
"prettier": "^3.2.5",
|
|
68
|
+
"typescript": "^5.5.4"
|
|
69
|
+
}
|
|
70
|
+
}
|