@akash-chowdhury-24/deployhub 2.0.21 → 2.0.23

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.
@@ -0,0 +1,247 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ /** Directories to skip when walking for wsgi.py / Flask entrypoints. */
5
+ const SKIP_DIRS = new Set([
6
+ 'venv',
7
+ '.venv',
8
+ 'env',
9
+ '.env',
10
+ 'node_modules',
11
+ '__pycache__',
12
+ '.git',
13
+ '.tox',
14
+ 'site-packages',
15
+ 'dist',
16
+ 'build',
17
+ '.eggs',
18
+ ]);
19
+
20
+ /**
21
+ * @param {string} startCommand
22
+ * @returns {string|null} e.g. `myapp.wsgi:application` or `app:app`
23
+ */
24
+ export function extractGunicornTarget(startCommand) {
25
+ if (!startCommand || typeof startCommand !== 'string') return null;
26
+ const tokens = startCommand.trim().split(/\s+/);
27
+ const gIdx = tokens.findIndex((t) => t === 'gunicorn' || t.endsWith('/gunicorn'));
28
+ if (gIdx < 0) return null;
29
+ for (let i = gIdx + 1; i < tokens.length; i++) {
30
+ const t = tokens[i];
31
+ if (t.startsWith('-')) {
32
+ // skip flag and its value when it looks like `--bind 0.0.0.0:8000`
33
+ if (
34
+ t === '-b' ||
35
+ t === '--bind' ||
36
+ t === '-c' ||
37
+ t === '--config' ||
38
+ t === '-n' ||
39
+ t === '--name' ||
40
+ t === '-p' ||
41
+ t === '--pid' ||
42
+ t === '-w' ||
43
+ t === '--workers' ||
44
+ t === '--chdir' ||
45
+ t === '-e' ||
46
+ t === '--env' ||
47
+ t === '--error-logfile' ||
48
+ t === '--access-logfile' ||
49
+ t === '--log-file'
50
+ ) {
51
+ i += 1;
52
+ }
53
+ continue;
54
+ }
55
+ // gunicorn app target is module:callable
56
+ if (/^[A-Za-z_][\w.]*:[A-Za-z_]\w*$/.test(t)) {
57
+ return t;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Walk cwd for files named `wsgi.py`, skipping venvs etc.
65
+ * @param {string} cwd
66
+ * @param {number} [maxDepth]
67
+ * @returns {string[]} absolute paths
68
+ */
69
+ function findWsgiFiles(cwd, maxDepth = 4) {
70
+ /** @type {string[]} */
71
+ const found = [];
72
+
73
+ /**
74
+ * @param {string} dir
75
+ * @param {number} depth
76
+ */
77
+ function walk(dir, depth) {
78
+ if (depth > maxDepth || found.length >= 20) return;
79
+ let entries;
80
+ try {
81
+ entries = fs.readdirSync(dir, { withFileTypes: true });
82
+ } catch {
83
+ return;
84
+ }
85
+ for (const ent of entries) {
86
+ if (ent.name.startsWith('.') && ent.name !== '.venv') {
87
+ // skip hidden except we already skip .venv via SKIP_DIRS
88
+ if (ent.isDirectory()) continue;
89
+ }
90
+ const full = path.join(dir, ent.name);
91
+ if (ent.isDirectory()) {
92
+ if (SKIP_DIRS.has(ent.name)) continue;
93
+ walk(full, depth + 1);
94
+ } else if (ent.isFile() && ent.name === 'wsgi.py') {
95
+ found.push(full);
96
+ }
97
+ }
98
+ }
99
+
100
+ walk(cwd, 0);
101
+ return found;
102
+ }
103
+
104
+ /**
105
+ * Convert absolute wsgi.py path under cwd to gunicorn target `pkg.wsgi:application`.
106
+ * @param {string} cwd
107
+ * @param {string} wsgiAbs
108
+ * @returns {string|null}
109
+ */
110
+ export function wsgiPathToGunicornTarget(cwd, wsgiAbs) {
111
+ const rel = path.relative(cwd, wsgiAbs);
112
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
113
+ const noExt = rel.replace(/\.py$/i, '');
114
+ const parts = noExt.split(/[/\\]/).filter(Boolean);
115
+ if (parts.length === 0) return null;
116
+ // Invalid if any segment is not a Python identifier
117
+ if (parts.some((p) => !/^[A-Za-z_]\w*$/.test(p))) return null;
118
+ return `${parts.join('.')}:application`;
119
+ }
120
+
121
+ /**
122
+ * Package directory containing wsgi.py relative to cwd (e.g. `myapp`), or null if root wsgi.py.
123
+ * @param {string} cwd
124
+ * @returns {string|null} relative dir name, or '' for root-level wsgi.py, or null if none
125
+ */
126
+ export function detectDjangoWsgiPackageDir(cwd) {
127
+ const wsgiAbs = pickPreferredWsgiFile(cwd);
128
+ if (!wsgiAbs) return null;
129
+ const rel = path.relative(cwd, path.dirname(wsgiAbs));
130
+ if (!rel || rel === '.') return '';
131
+ // Only copy the top-level package segment (myapp/wsgi.py → myapp)
132
+ const top = rel.split(/[/\\]/)[0];
133
+ return top || '';
134
+ }
135
+
136
+ /**
137
+ * Prefer standard startproject layout (one level down from manage.py), then config/, then any.
138
+ * @param {string} cwd
139
+ * @returns {string|null} absolute path to wsgi.py
140
+ */
141
+ function pickPreferredWsgiFile(cwd) {
142
+ const files = findWsgiFiles(cwd);
143
+ if (files.length === 0) return null;
144
+
145
+ const hasManage = fs.existsSync(path.join(cwd, 'manage.py'));
146
+
147
+ // Prefer <pkg>/wsgi.py directly under cwd when manage.py exists (django-admin startproject)
148
+ if (hasManage) {
149
+ const oneLevel = files.filter((f) => {
150
+ const rel = path.relative(cwd, f);
151
+ const parts = rel.split(/[/\\]/);
152
+ return parts.length === 2 && parts[1] === 'wsgi.py';
153
+ });
154
+ if (oneLevel.length === 1) return oneLevel[0];
155
+ // Prefer config/wsgi.py when present among one-level candidates (cookiecutter)
156
+ const configOne = oneLevel.find((f) => path.basename(path.dirname(f)) === 'config');
157
+ if (configOne) return configOne;
158
+ if (oneLevel.length > 0) return oneLevel[0];
159
+ }
160
+
161
+ const configWsgi = files.find((f) => {
162
+ const rel = path.relative(cwd, f).replace(/\\/g, '/');
163
+ return rel === 'config/wsgi.py';
164
+ });
165
+ if (configWsgi) return configWsgi;
166
+
167
+ const rootWsgi = files.find((f) => path.dirname(f) === path.resolve(cwd));
168
+ if (rootWsgi) return rootWsgi;
169
+
170
+ return files[0];
171
+ }
172
+
173
+ /**
174
+ * @param {string} [cwd]
175
+ * @returns {string} gunicorn target, e.g. `myapp.wsgi:application`
176
+ */
177
+ export function detectDjangoWsgiTarget(cwd = process.cwd()) {
178
+ const wsgiAbs = pickPreferredWsgiFile(cwd);
179
+ if (!wsgiAbs) return 'config.wsgi:application';
180
+ return wsgiPathToGunicornTarget(cwd, wsgiAbs) || 'config.wsgi:application';
181
+ }
182
+
183
+ /**
184
+ * @param {string} filePath
185
+ * @returns {'app'|'application'|null}
186
+ */
187
+ function detectFlaskCallableName(filePath) {
188
+ let content;
189
+ try {
190
+ content = fs.readFileSync(filePath, 'utf-8');
191
+ } catch {
192
+ return null;
193
+ }
194
+ // Prefer explicit assignment / factory patterns for `app` then `application`
195
+ if (
196
+ /\bapp\s*=\s*/.test(content) ||
197
+ /\bcreate_app\s*\(/.test(content) ||
198
+ /\bFlask\s*\(/.test(content)
199
+ ) {
200
+ // If both exist, prefer `application` only when `app` is absent as assignment
201
+ if (/\bapp\s*=\s*/.test(content) || /\bFlask\s*\(/.test(content)) {
202
+ if (/\bapplication\s*=\s*/.test(content) && !/\bapp\s*=\s*/.test(content)) {
203
+ return 'application';
204
+ }
205
+ return 'app';
206
+ }
207
+ }
208
+ if (/\bapplication\s*=\s*/.test(content)) return 'application';
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * @param {string} [cwd]
214
+ * @returns {string} gunicorn target, e.g. `app:app`
215
+ */
216
+ export function detectFlaskAppTarget(cwd = process.cwd()) {
217
+ const candidates = ['app.py', 'wsgi.py', 'application.py'];
218
+ for (const name of candidates) {
219
+ const full = path.join(cwd, name);
220
+ if (!fs.existsSync(full)) continue;
221
+ const callable = detectFlaskCallableName(full);
222
+ if (callable) {
223
+ const mod = name.replace(/\.py$/i, '');
224
+ return `${mod}:${callable}`;
225
+ }
226
+ }
227
+ // Module package app/__init__.py or app/app.py
228
+ const pkgInit = path.join(cwd, 'app', '__init__.py');
229
+ if (fs.existsSync(pkgInit)) {
230
+ const callable = detectFlaskCallableName(pkgInit);
231
+ if (callable) return `app:${callable}`;
232
+ }
233
+ const pkgApp = path.join(cwd, 'app', 'app.py');
234
+ if (fs.existsSync(pkgApp)) {
235
+ const callable = detectFlaskCallableName(pkgApp);
236
+ if (callable) return `app.app:${callable}`;
237
+ }
238
+ return 'app:app';
239
+ }
240
+
241
+ export default {
242
+ extractGunicornTarget,
243
+ detectDjangoWsgiTarget,
244
+ detectFlaskAppTarget,
245
+ detectDjangoWsgiPackageDir,
246
+ wsgiPathToGunicornTarget,
247
+ };