ports-manager 1.0.0 → 1.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/.claude/settings.json +12 -0
- package/README.md +278 -276
- package/package.json +8 -37
- package/src/cli.js +248 -278
- package/src/config.js +13 -20
- package/src/cors.js +95 -55
- package/src/detect.js +301 -114
- package/src/index.js +10 -9
- package/src/leases.js +130 -0
- package/src/preflight.js +214 -0
- package/src/process-manager.js +187 -97
- package/src/runtime.js +228 -121
- package/src/wiring.js +366 -0
- package/bridge/src/extension.ts +0 -96
- package/bridge/src/server.ts +0 -185
- package/dist/bridge/src/extension.js +0 -133
- package/dist/bridge/src/extension.js.map +0 -1
- package/dist/bridge/src/server.js +0 -202
- package/dist/bridge/src/server.js.map +0 -1
- package/src/bridge-client.js +0 -81
- package/src/ide.js +0 -107
- package/tsconfig.bridge.json +0 -18
package/src/cli.js
CHANGED
|
@@ -1,278 +1,248 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
12
|
-
const
|
|
13
|
-
const pkg = require('../package.json');
|
|
14
|
-
|
|
15
|
-
const HELP = `ports-manager [options]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (name === '--
|
|
69
|
-
else if (name === '--
|
|
70
|
-
else if (name === '--
|
|
71
|
-
else if (name === '--
|
|
72
|
-
else if (name === '--
|
|
73
|
-
else if (name === '--
|
|
74
|
-
else if (name === '--
|
|
75
|
-
else if (name === '--no-
|
|
76
|
-
else if (name === '--no-cors-
|
|
77
|
-
else if (name === '--
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
options
|
|
109
|
-
} else
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
if (options.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
keybinding: config.keybinding,
|
|
250
|
-
needsShim: corsEnabled,
|
|
251
|
-
withKeybinding: config.withKeybinding
|
|
252
|
-
});
|
|
253
|
-
console.log(`Generated ${generated.tasksFile}. Run “Ports Manager: Run All” from Tasks: Run Task.`);
|
|
254
|
-
if (generated.keybindingsFile) {
|
|
255
|
-
console.log(`Suggested shortcut ${config.keybinding} is in ${generated.keybindingsFile}; copy it into Preferences: Open Keyboard Shortcuts (JSON).`);
|
|
256
|
-
console.log('VS Code/Cursor does not apply workspace keybindings files automatically.');
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let proxy;
|
|
262
|
-
try {
|
|
263
|
-
if (config.proxy) proxy = await startProxy({
|
|
264
|
-
port: ports.proxy, frontendPort: ports.frontend, backendPort: ports.backend,
|
|
265
|
-
apiPrefix: config.apiPrefix
|
|
266
|
-
});
|
|
267
|
-
const code = await runProcesses(specs, {
|
|
268
|
-
waitForBackend: config.waitForBackend,
|
|
269
|
-
backendPort: ports.backend
|
|
270
|
-
});
|
|
271
|
-
process.exitCode = code;
|
|
272
|
-
} finally {
|
|
273
|
-
if (proxy) await proxy.close();
|
|
274
|
-
if (temporaryShim) temporaryShim.cleanup();
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
module.exports = { HELP, main, parseArgs, proxySpec, runProxyOnly };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { loadConfig, mergeConfig } = require('./config');
|
|
5
|
+
const { detectFolders, classifyProject, naturalPort } = require('./detect');
|
|
6
|
+
const { allocatePorts, appendNodeRequire, buildSpecs } = require('./runtime');
|
|
7
|
+
const { checkProject, describeFailure, installDependencies, summarize } = require('./preflight');
|
|
8
|
+
const { applyWiring, planWiring, wiringMode } = require('./wiring');
|
|
9
|
+
const { createCorsShim } = require('./cors');
|
|
10
|
+
const { runProcesses } = require('./process-manager');
|
|
11
|
+
const { startProxy } = require('./proxy');
|
|
12
|
+
const leases = require('./leases');
|
|
13
|
+
const pkg = require('../package.json');
|
|
14
|
+
|
|
15
|
+
const HELP = `ports-manager [options]
|
|
16
|
+
|
|
17
|
+
Run a paired frontend/backend project on free ports, with the frontend actually
|
|
18
|
+
wired to wherever the backend landed.
|
|
19
|
+
|
|
20
|
+
--frontend-dir PATH Override frontend/client directory
|
|
21
|
+
--backend-dir PATH Override backend/server directory
|
|
22
|
+
--frontend-port PORT Pin the frontend port
|
|
23
|
+
--backend-port PORT Pin the backend port
|
|
24
|
+
--proxy-port PORT Pin the same-origin proxy port
|
|
25
|
+
--range MIN-MAX Port search range (default 3000-5999)
|
|
26
|
+
--ban PORTS Never use these comma-separated ports
|
|
27
|
+
--proxy / --no-proxy Force or forbid same-origin proxy mode
|
|
28
|
+
--api-prefix PATH Path routed to the backend (default /api)
|
|
29
|
+
--no-install Fail instead of installing missing dependencies
|
|
30
|
+
--no-cors Do not inject backend CORS origin variables
|
|
31
|
+
--force-cors Also preload a response-time CORS shim
|
|
32
|
+
--no-cors-credentials Shim omits Access-Control-Allow-Credentials
|
|
33
|
+
--wait-for-backend[=MS] Wait for the backend before starting the frontend
|
|
34
|
+
--config PATH Explicit JSON config
|
|
35
|
+
--dry-run Show the plan without installing or starting anything
|
|
36
|
+
--help, -h Show help
|
|
37
|
+
--version, -v Show version and author
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
function valueFor(argv, index, name) {
|
|
41
|
+
const argument = argv[index];
|
|
42
|
+
const equals = argument.indexOf('=');
|
|
43
|
+
if (equals !== -1) return { value: argument.slice(equals + 1), consumed: 0 };
|
|
44
|
+
if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) {
|
|
45
|
+
throw new Error(`${name} requires a value`);
|
|
46
|
+
}
|
|
47
|
+
return { value: argv[index + 1], consumed: 1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function integer(value, name) {
|
|
51
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name} must be an integer`);
|
|
52
|
+
return Number(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function boolean(value, name) {
|
|
56
|
+
if (value === 'true') return true;
|
|
57
|
+
if (value === 'false') return false;
|
|
58
|
+
throw new Error(`${name} must be true or false`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArgs(argv) {
|
|
62
|
+
const options = { bannedPorts: [] };
|
|
63
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
64
|
+
const argument = argv[index];
|
|
65
|
+
const name = argument.split('=')[0];
|
|
66
|
+
if (name === '--help' || name === '-h') options.help = true;
|
|
67
|
+
else if (name === '--version' || name === '-v') options.version = true;
|
|
68
|
+
else if (name === '--dry-run') options.dryRun = true;
|
|
69
|
+
else if (name === '--proxy') options.proxy = true;
|
|
70
|
+
else if (name === '--no-proxy') options.proxy = false;
|
|
71
|
+
else if (name === '--cors') options.cors = true;
|
|
72
|
+
else if (name === '--no-cors' || name === '--no-cors-shim') options.cors = false;
|
|
73
|
+
else if (name === '--force-cors') options.forceCors = true;
|
|
74
|
+
else if (name === '--install') options.install = true;
|
|
75
|
+
else if (name === '--no-install') options.install = false;
|
|
76
|
+
else if (name === '--no-cors-credentials') options.corsCredentials = false;
|
|
77
|
+
else if (name === '--wait-for-backend') {
|
|
78
|
+
if (argument.includes('=')) options.waitForBackend = integer(argument.split('=').slice(1).join('='), name);
|
|
79
|
+
else if (argv[index + 1] && /^\d+$/.test(argv[index + 1])) options.waitForBackend = integer(argv[++index], name);
|
|
80
|
+
else options.waitForBackend = 10000;
|
|
81
|
+
} else {
|
|
82
|
+
const mapping = {
|
|
83
|
+
'--frontend': 'frontend', '--backend': 'backend',
|
|
84
|
+
'--frontend-dir': 'frontend', '--backend-dir': 'backend', '--config': 'configPath',
|
|
85
|
+
'--api-prefix': 'apiPrefix',
|
|
86
|
+
'--frontend-port': 'frontendPort', '--backend-port': 'backendPort',
|
|
87
|
+
'--proxy-port': 'proxyPort', '--cors-credentials': 'corsCredentials',
|
|
88
|
+
'--port-range': 'portRange', '--range': 'portRange',
|
|
89
|
+
'--ban-port': 'banPort', '--ban': 'ban'
|
|
90
|
+
};
|
|
91
|
+
const key = mapping[name];
|
|
92
|
+
if (!key) throw new Error(`unknown option: ${argument}`);
|
|
93
|
+
const found = valueFor(argv, index, name);
|
|
94
|
+
index += found.consumed;
|
|
95
|
+
if (['frontendPort', 'backendPort', 'proxyPort'].includes(key)) {
|
|
96
|
+
options[key] = integer(found.value, name);
|
|
97
|
+
} else if (key === 'corsCredentials') {
|
|
98
|
+
options[key] = boolean(found.value, name);
|
|
99
|
+
} else if (key === 'banPort') {
|
|
100
|
+
options.bannedPorts.push(integer(found.value, name));
|
|
101
|
+
} else if (key === 'ban') {
|
|
102
|
+
const ports = found.value.split(',').map((value) => value.trim()).filter(Boolean);
|
|
103
|
+
if (!ports.length) throw new Error('--ban requires a comma-separated port list');
|
|
104
|
+
options.bannedPorts.push(...ports.map((value) => integer(value, name)));
|
|
105
|
+
} else if (key === 'portRange') {
|
|
106
|
+
const match = found.value.match(/^(\d+)-(\d+)$/);
|
|
107
|
+
if (!match) throw new Error('--port-range must be MIN-MAX');
|
|
108
|
+
options.portRange = [Number(match[1]), Number(match[2])];
|
|
109
|
+
} else options[key] = found.value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!options.bannedPorts.length) delete options.bannedPorts;
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Installs anything missing so we never hand an unrunnable command to the shell. */
|
|
117
|
+
async function preflight(projects, config, logger = console) {
|
|
118
|
+
const checks = projects.map(checkProject);
|
|
119
|
+
const broken = checks.filter((check) => !check.ok);
|
|
120
|
+
if (!broken.length) return checks;
|
|
121
|
+
|
|
122
|
+
if (config.install === false) {
|
|
123
|
+
const detail = broken.map(describeFailure).join('\n');
|
|
124
|
+
throw new Error(`dependencies are missing and --no-install was given\n${detail}`);
|
|
125
|
+
}
|
|
126
|
+
for (const check of broken) {
|
|
127
|
+
logger.log(`${check.role}: ${summarize(check)}`);
|
|
128
|
+
const code = await installDependencies(check, logger);
|
|
129
|
+
if (code !== 0) throw new Error(`${check.packageManager} install failed in ${check.directory}`);
|
|
130
|
+
}
|
|
131
|
+
const rechecked = projects.map(checkProject);
|
|
132
|
+
const stillBroken = rechecked.filter((check) => !check.ok);
|
|
133
|
+
if (stillBroken.length) {
|
|
134
|
+
throw new Error(`still missing after install:\n${stillBroken.map(describeFailure).join('\n')}`);
|
|
135
|
+
}
|
|
136
|
+
return rechecked;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function describePort(decision) {
|
|
140
|
+
if (!decision) return '';
|
|
141
|
+
if (decision.requested) return `${decision.port} (pinned)`;
|
|
142
|
+
if (!decision.fellBack) return `${decision.port}`;
|
|
143
|
+
return `${decision.port} (${decision.natural} busy)`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function printPlan(root, frontend, backend, decisions, wiring, config) {
|
|
147
|
+
const byName = Object.fromEntries(decisions.map((decision) => [decision.name, decision]));
|
|
148
|
+
console.log(`Root: ${root}`);
|
|
149
|
+
console.log(`Backend: ${backend.framework} / npm run ${backend.script} / port ${describePort(byName.backend)}`);
|
|
150
|
+
console.log(`Frontend: ${frontend.framework} / npm run ${frontend.script} / port ${describePort(byName.frontend)}`);
|
|
151
|
+
console.log(`Wiring: ${wiring.describe()}`);
|
|
152
|
+
console.log(`CORS: ${wiring.sameOrigin
|
|
153
|
+
? 'not needed (same origin)'
|
|
154
|
+
: (wiring.corsEnvNames.length ? `injecting ${wiring.corsEnvNames.join(', ')}` : 'cross-origin')}${
|
|
155
|
+
config.forceCors ? ' + preload shim' : ''}`);
|
|
156
|
+
for (const note of wiring.notes) console.log(` ${note}`);
|
|
157
|
+
console.log(`Open: ${wiring.browserUrl}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function main(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
161
|
+
const options = parseArgs(argv);
|
|
162
|
+
if (options.help) return console.log(HELP);
|
|
163
|
+
if (options.version) {
|
|
164
|
+
return console.log(`ports-manager v${pkg.version}\nMuhammad Saad Amin (SENODROOM)`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const root = path.resolve(cwd);
|
|
168
|
+
const loaded = await loadConfig(root, options.configPath);
|
|
169
|
+
const config = mergeConfig(loaded.config, options);
|
|
170
|
+
const folders = detectFolders(root, config);
|
|
171
|
+
const frontend = classifyProject(folders.frontend, 'frontend');
|
|
172
|
+
const backend = classifyProject(folders.backend, 'backend');
|
|
173
|
+
|
|
174
|
+
if (!options.dryRun) await preflight([frontend, backend], config);
|
|
175
|
+
else {
|
|
176
|
+
for (const check of [frontend, backend].map(checkProject).filter((entry) => !entry.ok)) {
|
|
177
|
+
console.warn(`Would install first — ${describeFailure(check)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const naturalBackendPort = naturalPort(backend);
|
|
182
|
+
const naturalFrontendPort = naturalPort(frontend);
|
|
183
|
+
const needsProxy = wiringMode(frontend, config) === 'proxy';
|
|
184
|
+
|
|
185
|
+
const requests = [
|
|
186
|
+
{ name: 'backend', requested: config.backendPort, natural: naturalBackendPort },
|
|
187
|
+
{ name: 'frontend', requested: config.frontendPort, natural: naturalFrontendPort }
|
|
188
|
+
];
|
|
189
|
+
if (needsProxy) requests.push({ name: 'proxy', requested: config.proxyPort, natural: undefined });
|
|
190
|
+
|
|
191
|
+
const allocation = await allocatePorts(requests, config);
|
|
192
|
+
const { ports, decisions } = allocation;
|
|
193
|
+
let leased = false;
|
|
194
|
+
let cleanupWiring = () => {};
|
|
195
|
+
let temporaryShim;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const wiring = planWiring({ frontend, backend, ports, config, naturalBackendPort });
|
|
199
|
+
|
|
200
|
+
if (config.forceCors && !options.dryRun) {
|
|
201
|
+
temporaryShim = createCorsShim();
|
|
202
|
+
wiring.backendEnv.PORTS_MANAGER_CORS_ORIGIN = wiring.browserUrl;
|
|
203
|
+
wiring.backendEnv.PORTS_MANAGER_CORS_CREDENTIALS = String(config.corsCredentials);
|
|
204
|
+
wiring.backendEnv.NODE_OPTIONS = appendNodeRequire(process.env.NODE_OPTIONS || '', temporaryShim.filename);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
printPlan(root, frontend, backend, decisions, wiring, config);
|
|
208
|
+
if (options.dryRun) {
|
|
209
|
+
console.log('Dry run: nothing installed, written, or started.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
cleanupWiring = applyWiring(wiring, frontend, ports, config);
|
|
214
|
+
const specs = buildSpecs(frontend, backend, ports, config, wiring);
|
|
215
|
+
leased = leases.acquire(Object.values(ports), root);
|
|
216
|
+
|
|
217
|
+
// Hand the ports over to the children only now that everything else is ready.
|
|
218
|
+
await allocation.release();
|
|
219
|
+
|
|
220
|
+
let proxy;
|
|
221
|
+
try {
|
|
222
|
+
if (wiring.useProxy) {
|
|
223
|
+
proxy = await startProxy({
|
|
224
|
+
port: ports.proxy, frontendPort: ports.frontend, backendPort: ports.backend,
|
|
225
|
+
apiPrefix: config.apiPrefix
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const code = await runProcesses(specs, {
|
|
229
|
+
waitForBackend: config.waitForBackend,
|
|
230
|
+
backendPort: ports.backend,
|
|
231
|
+
frontendPort: ports.frontend,
|
|
232
|
+
onReady({ backendUp, frontendUp }) {
|
|
233
|
+
if (backendUp && frontendUp) console.log(`\nReady: ${wiring.browserUrl} -> ${wiring.describe()}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
process.exitCode = code;
|
|
237
|
+
} finally {
|
|
238
|
+
if (proxy) await proxy.close();
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
await allocation.release();
|
|
242
|
+
cleanupWiring();
|
|
243
|
+
if (temporaryShim) temporaryShim.cleanup();
|
|
244
|
+
if (leased) leases.release();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { HELP, main, parseArgs, preflight, printPlan };
|
package/src/config.js
CHANGED
|
@@ -6,24 +6,25 @@ const { cosmiconfig } = require('cosmiconfig');
|
|
|
6
6
|
|
|
7
7
|
const DEFAULTS = Object.freeze({
|
|
8
8
|
pairs: [['frontend', 'backend'], ['client', 'server']],
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Wide enough to cover the ports MERN projects naturally want (3000, 5000, 5173...)
|
|
10
|
+
// and to keep finding free ones when several projects run at once.
|
|
11
|
+
portRange: [3000, 5999],
|
|
12
|
+
bannedPorts: [],
|
|
11
13
|
cors: true,
|
|
12
14
|
forceCors: false,
|
|
13
15
|
corsCredentials: true,
|
|
14
|
-
proxy
|
|
16
|
+
// null = decide automatically (used only when the frontend cannot proxy for itself).
|
|
17
|
+
proxy: null,
|
|
15
18
|
apiPrefix: '/api',
|
|
16
|
-
|
|
19
|
+
install: true,
|
|
17
20
|
waitForBackend: 0,
|
|
18
|
-
withKeybinding: false,
|
|
19
|
-
keybinding: 'ctrl+alt+p',
|
|
20
21
|
env: {}
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
const allowedKeys = new Set([
|
|
24
25
|
'pairs', 'frontend', 'backend', 'frontendPort', 'backendPort', 'proxyPort', 'portRange',
|
|
25
26
|
'bannedPorts', 'cors', 'forceCors', 'corsCredentials', 'proxy', 'apiPrefix',
|
|
26
|
-
'
|
|
27
|
+
'install', 'waitForBackend', 'env'
|
|
27
28
|
]);
|
|
28
29
|
|
|
29
30
|
function normalizeConfig(input) {
|
|
@@ -36,13 +37,6 @@ function normalizeConfig(input) {
|
|
|
36
37
|
if (config.proxy && typeof config.proxy === 'object' && !Array.isArray(config.proxy)) {
|
|
37
38
|
config.proxy = config.proxy.enabled === true;
|
|
38
39
|
}
|
|
39
|
-
if (config.ideTerminals && typeof config.ideTerminals === 'object' &&
|
|
40
|
-
!Array.isArray(config.ideTerminals)) {
|
|
41
|
-
const ide = config.ideTerminals;
|
|
42
|
-
config.ideTerminals = ide.mode || 'off';
|
|
43
|
-
if (ide.withKeybinding !== undefined) config.withKeybinding = ide.withKeybinding;
|
|
44
|
-
if (ide.keybinding !== undefined) config.keybinding = ide.keybinding;
|
|
45
|
-
}
|
|
46
40
|
return config;
|
|
47
41
|
}
|
|
48
42
|
|
|
@@ -80,19 +74,18 @@ function validateConfig(config) {
|
|
|
80
74
|
if (!Array.isArray(config.bannedPorts)) throw new Error('bannedPorts must be an array');
|
|
81
75
|
config.bannedPorts.forEach((port, index) => assertPort(port, `bannedPorts[${index}]`));
|
|
82
76
|
}
|
|
83
|
-
for (const key of ['cors', 'forceCors', 'corsCredentials', '
|
|
77
|
+
for (const key of ['cors', 'forceCors', 'corsCredentials', 'install']) {
|
|
84
78
|
if (config[key] !== undefined && typeof config[key] !== 'boolean') {
|
|
85
79
|
throw new Error(`${key} must be boolean`);
|
|
86
80
|
}
|
|
87
81
|
}
|
|
82
|
+
if (config.proxy !== undefined && config.proxy !== null && typeof config.proxy !== 'boolean') {
|
|
83
|
+
throw new Error('proxy must be boolean or null');
|
|
84
|
+
}
|
|
88
85
|
if (config.apiPrefix !== undefined &&
|
|
89
86
|
(typeof config.apiPrefix !== 'string' || !config.apiPrefix.startsWith('/'))) {
|
|
90
87
|
throw new Error('apiPrefix must be a string beginning with /');
|
|
91
88
|
}
|
|
92
|
-
if (config.ideTerminals !== undefined &&
|
|
93
|
-
!['off', 'auto', 'tasks', 'extension'].includes(config.ideTerminals)) {
|
|
94
|
-
throw new Error('ideTerminals must be off, auto, tasks, or extension');
|
|
95
|
-
}
|
|
96
89
|
if (config.waitForBackend !== undefined &&
|
|
97
90
|
(!Number.isInteger(config.waitForBackend) || config.waitForBackend < 0)) {
|
|
98
91
|
throw new Error('waitForBackend must be a non-negative integer (milliseconds)');
|
|
@@ -142,7 +135,7 @@ function mergeConfig(fileConfig, cliConfig) {
|
|
|
142
135
|
if (allowedKeys.has(key) && value !== undefined) result[key] = value;
|
|
143
136
|
}
|
|
144
137
|
result.bannedPorts = [...new Set([
|
|
145
|
-
|
|
138
|
+
...(fileConfig.bannedPorts || []), ...(cliConfig.bannedPorts || [])
|
|
146
139
|
])];
|
|
147
140
|
validateConfig(result);
|
|
148
141
|
return result;
|