ports-manager 1.0.0 → 1.0.1
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 +281 -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 +261 -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/cors.js
CHANGED
|
@@ -1,55 +1,95 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Last-resort CORS shim, enabled only by --force-cors.
|
|
9
|
+
*
|
|
10
|
+
* The previous version set headers before the app ran, so any app using the `cors`
|
|
11
|
+
* middleware simply overwrote them. This one patches writeHead/end so the headers are
|
|
12
|
+
* applied at send time — including stripping Access-Control-* keys passed inline to
|
|
13
|
+
* writeHead — which is the only way to win against the app's own middleware.
|
|
14
|
+
*/
|
|
15
|
+
const SHIM_SOURCE = String.raw`'use strict';
|
|
16
|
+
const http = require('node:http');
|
|
17
|
+
|
|
18
|
+
function mergeVary(current, value) {
|
|
19
|
+
const values = String(current || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
20
|
+
if (!values.some((item) => item.toLowerCase() === value.toLowerCase())) values.push(value);
|
|
21
|
+
return values.join(', ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function corsHeaders(req) {
|
|
25
|
+
const origin = (req.headers && req.headers.origin) || process.env.PORTS_MANAGER_CORS_ORIGIN;
|
|
26
|
+
if (!origin) return null;
|
|
27
|
+
const headers = {
|
|
28
|
+
'Access-Control-Allow-Origin': origin,
|
|
29
|
+
'Access-Control-Allow-Methods':
|
|
30
|
+
req.headers['access-control-request-method'] || 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
|
31
|
+
'Access-Control-Allow-Headers':
|
|
32
|
+
req.headers['access-control-request-headers'] || 'Content-Type,Authorization'
|
|
33
|
+
};
|
|
34
|
+
if (process.env.PORTS_MANAGER_CORS_CREDENTIALS !== 'false') {
|
|
35
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function applyHeaders(req, res, inlineHeaders) {
|
|
41
|
+
const headers = corsHeaders(req);
|
|
42
|
+
if (!headers) return;
|
|
43
|
+
if (inlineHeaders && typeof inlineHeaders === 'object' && !Array.isArray(inlineHeaders)) {
|
|
44
|
+
for (const key of Object.keys(inlineHeaders)) {
|
|
45
|
+
if (key.toLowerCase().indexOf('access-control-') === 0) delete inlineHeaders[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (res.headersSent) return;
|
|
49
|
+
for (const [key, value] of Object.entries(headers)) res.setHeader(key, value);
|
|
50
|
+
res.setHeader('Vary', mergeVary(res.getHeader('Vary'), 'Origin'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const originalEmit = http.Server.prototype.emit;
|
|
54
|
+
http.Server.prototype.emit = function portsManagerEmit(event, req, res) {
|
|
55
|
+
if (event !== 'request' || !req || !res) {
|
|
56
|
+
return originalEmit.apply(this, arguments);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const originalWriteHead = res.writeHead;
|
|
60
|
+
res.writeHead = function patchedWriteHead(...args) {
|
|
61
|
+
const inline = args.find((value) => value && typeof value === 'object');
|
|
62
|
+
applyHeaders(req, res, inline);
|
|
63
|
+
return originalWriteHead.apply(this, args);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const originalEnd = res.end;
|
|
67
|
+
res.end = function patchedEnd(...args) {
|
|
68
|
+
applyHeaders(req, res, null);
|
|
69
|
+
return originalEnd.apply(this, args);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (req.method === 'OPTIONS') {
|
|
73
|
+
applyHeaders(req, res, null);
|
|
74
|
+
res.statusCode = 204;
|
|
75
|
+
res.setHeader('Content-Length', '0');
|
|
76
|
+
originalEnd.call(res);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return originalEmit.apply(this, arguments);
|
|
80
|
+
};
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
function createCorsShim() {
|
|
84
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ports-manager-'));
|
|
85
|
+
const filename = path.join(directory, 'cors-preload.cjs');
|
|
86
|
+
fs.writeFileSync(filename, SHIM_SOURCE, { encoding: 'utf8', mode: 0o600 });
|
|
87
|
+
return {
|
|
88
|
+
filename,
|
|
89
|
+
cleanup() {
|
|
90
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { SHIM_SOURCE, createCorsShim };
|
package/src/detect.js
CHANGED
|
@@ -1,114 +1,281 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts']);
|
|
7
|
+
const SKIP_DIRECTORIES = new Set([
|
|
8
|
+
'node_modules', 'dist', 'build', 'out', '.git', '.next', '.cache', 'coverage', '.vite', 'public'
|
|
9
|
+
]);
|
|
10
|
+
const MAX_SOURCE_FILES = 80;
|
|
11
|
+
const MAX_SOURCE_BYTES = 256 * 1024;
|
|
12
|
+
|
|
13
|
+
/** Origin env names backends conventionally read for their CORS allowlist. */
|
|
14
|
+
const CORS_ENV_NAMES = [
|
|
15
|
+
'CORS_ORIGIN', 'CORS_ORIGINS', 'CORS_ALLOWED_ORIGINS', 'ALLOWED_ORIGINS',
|
|
16
|
+
'CLIENT_URL', 'CLIENT_ORIGIN', 'FRONTEND_URL', 'FRONTEND_ORIGIN', 'APP_URL', 'WEB_ORIGIN'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const VITE_CONFIG_NAMES = [
|
|
20
|
+
'vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs',
|
|
21
|
+
'vite.config.cts', 'vite.config.cjs'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const FRAMEWORK_DEFAULT_PORTS = { vite: 5173, cra: 3000, next: 3000, generic: 3000 };
|
|
25
|
+
|
|
26
|
+
function readPackage(directory) {
|
|
27
|
+
const filename = path.join(directory, 'package.json');
|
|
28
|
+
if (!fs.existsSync(filename)) return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`invalid package.json at ${filename}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validProject(directory) {
|
|
37
|
+
return fs.existsSync(directory) && fs.statSync(directory).isDirectory() && Boolean(readPackage(directory));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectFolders(cwd, overrides = {}) {
|
|
41
|
+
const pairs = overrides.pairs || [['frontend', 'backend'], ['client', 'server']];
|
|
42
|
+
const found = pairs.filter(([front, back]) =>
|
|
43
|
+
validProject(path.join(cwd, front)) && validProject(path.join(cwd, back)));
|
|
44
|
+
|
|
45
|
+
let frontend = overrides.frontend && path.resolve(cwd, overrides.frontend);
|
|
46
|
+
let backend = overrides.backend && path.resolve(cwd, overrides.backend);
|
|
47
|
+
|
|
48
|
+
if (!frontend && !backend) {
|
|
49
|
+
if (found.length > 1) {
|
|
50
|
+
throw new Error('ambiguous project layout: multiple configured folder pairs exist; use --frontend-dir and --backend-dir');
|
|
51
|
+
}
|
|
52
|
+
if (found.length === 1) {
|
|
53
|
+
frontend = path.join(cwd, found[0][0]);
|
|
54
|
+
backend = path.join(cwd, found[0][1]);
|
|
55
|
+
} else {
|
|
56
|
+
const basename = path.basename(cwd).toLowerCase();
|
|
57
|
+
for (const [front, back] of pairs) {
|
|
58
|
+
if (basename === front || basename === back) {
|
|
59
|
+
const parent = path.dirname(cwd);
|
|
60
|
+
if (validProject(path.join(parent, front)) && validProject(path.join(parent, back))) {
|
|
61
|
+
throw new Error(`run ports-manager from the parent directory: ${parent}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error('no paired frontend/backend or client/server package.json projects found');
|
|
66
|
+
}
|
|
67
|
+
} else if (!frontend || !backend) {
|
|
68
|
+
const wanted = frontend ? 'backend' : 'frontend';
|
|
69
|
+
const candidates = pairs.map((pair) => pair[wanted === 'frontend' ? 0 : 1])
|
|
70
|
+
.map((name) => path.join(cwd, name)).filter(validProject);
|
|
71
|
+
if (candidates.length !== 1) throw new Error(`cannot infer ${wanted}; specify --${wanted}-dir`);
|
|
72
|
+
if (wanted === 'frontend') frontend = candidates[0];
|
|
73
|
+
else backend = candidates[0];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!validProject(frontend)) throw new Error(`frontend must be a directory containing package.json: ${frontend}`);
|
|
77
|
+
if (!validProject(backend)) throw new Error(`backend must be a directory containing package.json: ${backend}`);
|
|
78
|
+
if (path.resolve(frontend) === path.resolve(backend)) throw new Error('frontend and backend must be distinct directories');
|
|
79
|
+
return { frontend, backend };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasDependency(pkg, name) {
|
|
83
|
+
return Boolean((pkg.dependencies && pkg.dependencies[name]) ||
|
|
84
|
+
(pkg.devDependencies && pkg.devDependencies[name]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function classifyProject(directory, role) {
|
|
88
|
+
const pkg = readPackage(directory);
|
|
89
|
+
const scripts = pkg.scripts || {};
|
|
90
|
+
const script = scripts.dev ? 'dev' : scripts.start ? 'start' : null;
|
|
91
|
+
if (!script) throw new Error(`${directory} has neither a dev nor start script`);
|
|
92
|
+
|
|
93
|
+
let framework = 'generic';
|
|
94
|
+
if (role === 'frontend') {
|
|
95
|
+
if (hasDependency(pkg, 'react-scripts')) framework = 'cra';
|
|
96
|
+
else if (hasDependency(pkg, 'vite')) framework = 'vite';
|
|
97
|
+
else if (hasDependency(pkg, 'next')) framework = 'next';
|
|
98
|
+
} else {
|
|
99
|
+
for (const [dependency, name] of [
|
|
100
|
+
['@nestjs/core', 'nest'], ['express', 'express'], ['koa', 'koa'], ['fastify', 'fastify']
|
|
101
|
+
]) {
|
|
102
|
+
if (hasDependency(pkg, dependency)) {
|
|
103
|
+
framework = name;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { directory, pkg, role, framework, script, scriptText: scripts[script] || '' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reads a bounded sample of a project's own source. Replaces the old three-filename
|
|
113
|
+
* guess, which reported "no process.env.PORT" for any backend that reads its port
|
|
114
|
+
* through a config module.
|
|
115
|
+
*/
|
|
116
|
+
function collectSources(directory, limit = MAX_SOURCE_FILES) {
|
|
117
|
+
const files = [];
|
|
118
|
+
const queue = [directory];
|
|
119
|
+
while (queue.length && files.length < limit) {
|
|
120
|
+
const current = queue.shift();
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (files.length >= limit) break;
|
|
129
|
+
const full = path.join(current, entry.name);
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
if (!SKIP_DIRECTORIES.has(entry.name) && !entry.name.startsWith('.')) queue.push(full);
|
|
132
|
+
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.statSync(full).size > MAX_SOURCE_BYTES) continue;
|
|
135
|
+
files.push({ file: full, source: fs.readFileSync(full, 'utf8') });
|
|
136
|
+
} catch {
|
|
137
|
+
/* unreadable file; skip */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readDotEnv(directory) {
|
|
146
|
+
const values = {};
|
|
147
|
+
for (const name of ['.env.local', '.env.development', '.env']) {
|
|
148
|
+
const filename = path.join(directory, name);
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = fs.readFileSync(filename, 'utf8');
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
156
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
157
|
+
if (!match) continue;
|
|
158
|
+
const key = match[1];
|
|
159
|
+
if (values[key] !== undefined) continue;
|
|
160
|
+
values[key] = match[2].trim().replace(/^["']|["']$/g, '').replace(/\s+#.*$/, '');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return values;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function firstPort(patterns, source) {
|
|
167
|
+
for (const pattern of patterns) {
|
|
168
|
+
const match = source.match(pattern);
|
|
169
|
+
if (match) {
|
|
170
|
+
const port = Number(match[1]);
|
|
171
|
+
if (Number.isInteger(port) && port >= 1 && port <= 65535) return port;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function findViteConfig(directory) {
|
|
178
|
+
for (const name of VITE_CONFIG_NAMES) {
|
|
179
|
+
const filename = path.join(directory, name);
|
|
180
|
+
if (fs.existsSync(filename)) return filename;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The port this project would use on its own. Preferring it keeps a solo project's URLs
|
|
187
|
+
* (and OAuth redirect URIs) stable, while a second project falls back to a free port.
|
|
188
|
+
*/
|
|
189
|
+
function naturalPort(project) {
|
|
190
|
+
const env = readDotEnv(project.directory);
|
|
191
|
+
if (env.PORT && /^\d+$/.test(env.PORT)) return Number(env.PORT);
|
|
192
|
+
|
|
193
|
+
if (project.role === 'frontend') {
|
|
194
|
+
const fromScript = firstPort([/--port[ =]+(\d{2,5})/, /\s-p[ =]+(\d{2,5})/], project.scriptText || '');
|
|
195
|
+
if (fromScript) return fromScript;
|
|
196
|
+
const viteConfig = findViteConfig(project.directory);
|
|
197
|
+
if (viteConfig) {
|
|
198
|
+
try {
|
|
199
|
+
const fromConfig = firstPort([/\bport\s*:\s*(\d{2,5})/], fs.readFileSync(viteConfig, 'utf8'));
|
|
200
|
+
if (fromConfig) return fromConfig;
|
|
201
|
+
} catch {
|
|
202
|
+
/* unreadable config; fall through to the framework default */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return FRAMEWORK_DEFAULT_PORTS[project.framework] || 3000;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const source = collectSources(project.directory).map((entry) => entry.source).join('\n');
|
|
209
|
+
const fromSource = firstPort([
|
|
210
|
+
// `process.env.PORT || 5001`, and the wrapped forms: `Number(process.env.PORT) || 5001`,
|
|
211
|
+
// `parseInt(process.env.PORT ?? '5001')`.
|
|
212
|
+
/process\.env\.PORT\s*\)?\s*(?:\|\||\?\?)\s*["']?(\d{2,5})/,
|
|
213
|
+
/\bPORT\s*:\s*[^,\n]*?\.default\(\s*(\d{2,5})\s*\)/,
|
|
214
|
+
/\bPORT\s*[:=]\s*(\d{2,5})\b/,
|
|
215
|
+
/\.listen\s*\(\s*(\d{2,5})\b/
|
|
216
|
+
], source);
|
|
217
|
+
return fromSource || 5000;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Origin env names the backend actually references. Only these get injected, so we never
|
|
222
|
+
* invent a variable the app ignores.
|
|
223
|
+
*/
|
|
224
|
+
function scanCorsEnvNames(project) {
|
|
225
|
+
const source = collectSources(project.directory).map((entry) => entry.source).join('\n');
|
|
226
|
+
return CORS_ENV_NAMES.filter((name) => {
|
|
227
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
228
|
+
return new RegExp(`process\\.env\\.${escaped}\\b|process\\.env\\[['"\`]${escaped}['"\`]\\]|\\b${escaped}\\s*:`)
|
|
229
|
+
.test(source);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Frontend env names that carry a backend URL, e.g. VITE_API_BASE or REACT_APP_API_URL. */
|
|
234
|
+
function scanFrontendApiEnvNames(project) {
|
|
235
|
+
const source = collectSources(project.directory).map((entry) => entry.source).join('\n');
|
|
236
|
+
const names = new Set();
|
|
237
|
+
const patterns = [
|
|
238
|
+
/import\.meta\.env\.(VITE_[A-Z0-9_]+)/g,
|
|
239
|
+
/import\.meta\.env\[['"`](VITE_[A-Z0-9_]+)['"`]\]/g,
|
|
240
|
+
/process\.env\.(REACT_APP_[A-Z0-9_]+|NEXT_PUBLIC_[A-Z0-9_]+)/g
|
|
241
|
+
];
|
|
242
|
+
for (const pattern of patterns) {
|
|
243
|
+
for (const match of source.matchAll(pattern)) {
|
|
244
|
+
if (/API|BACKEND|SERVER|BASE_URL/.test(match[1])) names.add(match[1]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return [...names];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Whether the frontend already proxies API calls itself (Vite server.proxy, CRA `proxy`). */
|
|
251
|
+
function hasDevProxy(project) {
|
|
252
|
+
if (project.framework === 'cra' || project.framework === 'generic') {
|
|
253
|
+
return typeof project.pkg.proxy === 'string' && project.pkg.proxy.length > 0;
|
|
254
|
+
}
|
|
255
|
+
const viteConfig = findViteConfig(project.directory);
|
|
256
|
+
if (viteConfig) {
|
|
257
|
+
try {
|
|
258
|
+
return /\bproxy\s*:/.test(fs.readFileSync(viteConfig, 'utf8'));
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
CORS_ENV_NAMES,
|
|
268
|
+
FRAMEWORK_DEFAULT_PORTS,
|
|
269
|
+
classifyProject,
|
|
270
|
+
collectSources,
|
|
271
|
+
detectFolders,
|
|
272
|
+
findViteConfig,
|
|
273
|
+
hasDependency,
|
|
274
|
+
hasDevProxy,
|
|
275
|
+
naturalPort,
|
|
276
|
+
readDotEnv,
|
|
277
|
+
readPackage,
|
|
278
|
+
scanCorsEnvNames,
|
|
279
|
+
scanFrontendApiEnvNames,
|
|
280
|
+
validProject
|
|
281
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
...require('./config'),
|
|
5
|
-
...require('./detect'),
|
|
6
|
-
...require('./
|
|
7
|
-
...require('./
|
|
8
|
-
...require('./
|
|
9
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
...require('./config'),
|
|
5
|
+
...require('./detect'),
|
|
6
|
+
...require('./preflight'),
|
|
7
|
+
...require('./runtime'),
|
|
8
|
+
...require('./wiring'),
|
|
9
|
+
...require('./leases')
|
|
10
|
+
};
|