swallowkit 1.0.0-beta.23 → 1.0.0-beta.25
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/README.ja.md +5 -0
- package/README.md +5 -0
- package/dist/cli/commands/dev.d.ts +3 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +118 -58
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +17 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +29 -19
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/utils/python-uv.d.ts +21 -0
- package/dist/utils/python-uv.d.ts.map +1 -0
- package/dist/utils/python-uv.js +111 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/dev.test.ts +46 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/cli/commands/dev.ts +170 -79
- package/src/cli/commands/init.ts +20 -1
- package/src/core/scaffold/native-schema-generator.ts +58 -29
- package/src/utils/python-uv.ts +96 -0
package/src/cli/commands/dev.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawn, ChildProcess } from 'child_process';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import * as https from 'https';
|
|
3
5
|
import * as path from 'path';
|
|
4
6
|
import * as fs from 'fs';
|
|
5
7
|
import * as os from 'os';
|
|
@@ -10,6 +12,15 @@ import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
|
10
12
|
import { applyDevSeedEnvironment, getContainerNameForModel, loadProjectModels } from './dev-seeds';
|
|
11
13
|
import { BackendLanguage } from '../../types';
|
|
12
14
|
import { detectFromProject, getCommands } from '../../utils/package-manager';
|
|
15
|
+
import {
|
|
16
|
+
buildProjectLocalUvEnv,
|
|
17
|
+
buildProjectLocalUvInstallerEnv,
|
|
18
|
+
buildUvPipInstallArgs,
|
|
19
|
+
buildUvVenvArgs,
|
|
20
|
+
getProjectLocalUvInstallerCommand,
|
|
21
|
+
getProjectLocalUvPaths,
|
|
22
|
+
getPythonProjectRoot,
|
|
23
|
+
} from '../../utils/python-uv';
|
|
13
24
|
import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
|
|
14
25
|
|
|
15
26
|
export interface DevOptions {
|
|
@@ -100,6 +111,36 @@ export function buildNextDevArgs(pm: string, port: string): string[] {
|
|
|
100
111
|
return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string {
|
|
115
|
+
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
|
|
119
|
+
return backendLanguage === 'csharp' ? 90_000 : 30_000;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function waitForHttpServerReady(
|
|
123
|
+
url: string,
|
|
124
|
+
timeoutMs = 30_000,
|
|
125
|
+
intervalMs = 500
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
const deadline = Date.now() + timeoutMs;
|
|
128
|
+
|
|
129
|
+
while (Date.now() <= deadline) {
|
|
130
|
+
if (await probeHttpServer(url)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Date.now() + intervalMs > deadline) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return probeHttpServer(url);
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
export function getPythonVirtualEnvPaths(functionsDir: string): {
|
|
104
145
|
venvDir: string;
|
|
105
146
|
binDir: string;
|
|
@@ -157,10 +198,20 @@ async function checkCoreTools(): Promise<boolean> {
|
|
|
157
198
|
return checkCommand('func', ['--version']);
|
|
158
199
|
}
|
|
159
200
|
|
|
160
|
-
async function checkCommand(
|
|
201
|
+
async function checkCommand(
|
|
202
|
+
command: string,
|
|
203
|
+
args: string[] = ['--version'],
|
|
204
|
+
options?: {
|
|
205
|
+
cwd?: string;
|
|
206
|
+
env?: NodeJS.ProcessEnv;
|
|
207
|
+
shell?: boolean;
|
|
208
|
+
}
|
|
209
|
+
): Promise<boolean> {
|
|
161
210
|
return new Promise((resolve) => {
|
|
162
211
|
const checkProcess = spawn(command, args, {
|
|
163
|
-
|
|
212
|
+
cwd: options?.cwd,
|
|
213
|
+
env: options?.env ?? process.env,
|
|
214
|
+
shell: options?.shell ?? true,
|
|
164
215
|
stdio: 'pipe',
|
|
165
216
|
});
|
|
166
217
|
|
|
@@ -174,26 +225,34 @@ async function checkCommand(command: string, args: string[] = ['--version']): Pr
|
|
|
174
225
|
});
|
|
175
226
|
}
|
|
176
227
|
|
|
177
|
-
async function
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const candidate of candidates) {
|
|
189
|
-
if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
|
|
190
|
-
return candidate;
|
|
191
|
-
}
|
|
228
|
+
async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
229
|
+
const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
|
|
230
|
+
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
231
|
+
|
|
232
|
+
if (await checkCommand('uv', ['--version'], { shell: false })) {
|
|
233
|
+
return { command: 'uv', env: uvEnv };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
|
|
237
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
192
238
|
}
|
|
193
239
|
|
|
194
|
-
|
|
195
|
-
|
|
240
|
+
console.log('📦 Installing project-local uv...');
|
|
241
|
+
const installer = getProjectLocalUvInstallerCommand();
|
|
242
|
+
await runCommand(
|
|
243
|
+
installer.command,
|
|
244
|
+
installer.args,
|
|
245
|
+
projectRoot,
|
|
246
|
+
'uv installation',
|
|
247
|
+
buildProjectLocalUvInstallerEnv(process.env, projectRoot),
|
|
248
|
+
false
|
|
196
249
|
);
|
|
250
|
+
|
|
251
|
+
if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
|
|
252
|
+
throw new Error('Failed to install project-local uv.');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
197
256
|
}
|
|
198
257
|
|
|
199
258
|
async function getCommandPath(command: string): Promise<string | null> {
|
|
@@ -256,6 +315,41 @@ async function captureCommandOutput(
|
|
|
256
315
|
});
|
|
257
316
|
}
|
|
258
317
|
|
|
318
|
+
async function probeHttpServer(url: string): Promise<boolean> {
|
|
319
|
+
return new Promise((resolve) => {
|
|
320
|
+
const target = new URL(url);
|
|
321
|
+
const requestFactory = target.protocol === 'https:' ? https.request : http.request;
|
|
322
|
+
let settled = false;
|
|
323
|
+
const finish = (value: boolean) => {
|
|
324
|
+
if (!settled) {
|
|
325
|
+
settled = true;
|
|
326
|
+
resolve(value);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const request = requestFactory(
|
|
331
|
+
{
|
|
332
|
+
hostname: target.hostname,
|
|
333
|
+
port: target.port,
|
|
334
|
+
path: target.pathname || '/',
|
|
335
|
+
method: 'GET',
|
|
336
|
+
timeout: 1000,
|
|
337
|
+
},
|
|
338
|
+
(response) => {
|
|
339
|
+
response.resume();
|
|
340
|
+
finish(true);
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
request.on('timeout', () => {
|
|
345
|
+
request.destroy();
|
|
346
|
+
finish(false);
|
|
347
|
+
});
|
|
348
|
+
request.on('error', () => finish(false));
|
|
349
|
+
request.end();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
259
353
|
async function resolvePythonRuntimeDetails(
|
|
260
354
|
functionsDir: string,
|
|
261
355
|
env: NodeJS.ProcessEnv
|
|
@@ -331,47 +425,36 @@ async function bridgePythonCoreToolsForWindowsArm64(
|
|
|
331
425
|
}
|
|
332
426
|
|
|
333
427
|
async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const bootstrap = await resolvePythonBootstrapCommand();
|
|
343
|
-
console.log(`📦 Creating Python virtual environment with ${bootstrap.label}...`);
|
|
344
|
-
await runCommand(
|
|
345
|
-
bootstrap.command,
|
|
346
|
-
[...bootstrap.argsPrefix, '-m', 'venv', '.venv'],
|
|
347
|
-
functionsDir,
|
|
348
|
-
'python virtual environment setup'
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
428
|
+
const projectRoot = getPythonProjectRoot(functionsDir);
|
|
429
|
+
const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
|
|
430
|
+
const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
431
|
+
const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
|
|
432
|
+
cwd: functionsDir,
|
|
433
|
+
env: uvEnv,
|
|
434
|
+
shell: false,
|
|
435
|
+
});
|
|
352
436
|
|
|
353
|
-
|
|
354
|
-
|
|
437
|
+
if (!hasUsableVirtualEnv) {
|
|
438
|
+
const venvArgs = buildUvVenvArgs('.venv');
|
|
439
|
+
if (fs.existsSync(venvDir)) {
|
|
440
|
+
venvArgs.push('--clear');
|
|
441
|
+
}
|
|
355
442
|
|
|
356
|
-
|
|
357
|
-
await runCommand(
|
|
358
|
-
'uv',
|
|
359
|
-
['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'],
|
|
360
|
-
functionsDir,
|
|
361
|
-
'python dependency installation',
|
|
362
|
-
pythonEnv
|
|
363
|
-
);
|
|
364
|
-
} else {
|
|
365
|
-
await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
|
|
366
|
-
await runCommand(
|
|
367
|
-
'python',
|
|
368
|
-
['-m', 'pip', 'install', '-r', 'requirements.txt'],
|
|
369
|
-
functionsDir,
|
|
370
|
-
'python dependency installation',
|
|
371
|
-
pythonEnv
|
|
372
|
-
);
|
|
443
|
+
console.log('📦 Creating Python virtual environment with uv...');
|
|
444
|
+
await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
|
|
373
445
|
}
|
|
374
446
|
|
|
447
|
+
console.log('📦 Installing Python Azure Functions dependencies with uv...');
|
|
448
|
+
await runCommand(
|
|
449
|
+
uvCommand,
|
|
450
|
+
buildUvPipInstallArgs(pythonExecutable, 'requirements.txt'),
|
|
451
|
+
functionsDir,
|
|
452
|
+
'python dependency installation',
|
|
453
|
+
uvEnv,
|
|
454
|
+
false
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
|
|
375
458
|
return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
|
|
376
459
|
}
|
|
377
460
|
|
|
@@ -556,6 +639,9 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
556
639
|
let mockServer: ConnectorMockServer | null = null;
|
|
557
640
|
let envLocalPath = '';
|
|
558
641
|
let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
|
|
642
|
+
const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
|
|
643
|
+
let functionsReadinessPromise: Promise<boolean> | null = null;
|
|
644
|
+
let functionsReady = !!options.noFunctions;
|
|
559
645
|
|
|
560
646
|
// Cleanup processes on Ctrl+C
|
|
561
647
|
process.on('SIGINT', async () => {
|
|
@@ -712,19 +798,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
712
798
|
await runCommand(pm, ['install'], functionsDir, `${pm} install`);
|
|
713
799
|
}
|
|
714
800
|
} else if (backendLanguage === 'csharp') {
|
|
715
|
-
|
|
716
|
-
for (const artifactPath of getCSharpFunctionsBuildArtifactPaths(functionsDir)) {
|
|
717
|
-
if (!fs.existsSync(artifactPath)) {
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
fs.rmSync(artifactPath, { recursive: true, force: true });
|
|
722
|
-
cleanedArtifacts = true;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (cleanedArtifacts) {
|
|
726
|
-
console.log('🧹 Cleaned previous C# Functions build artifacts before func start.');
|
|
727
|
-
}
|
|
801
|
+
console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
|
|
728
802
|
} else {
|
|
729
803
|
functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
|
|
730
804
|
}
|
|
@@ -839,7 +913,11 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
839
913
|
}
|
|
840
914
|
});
|
|
841
915
|
|
|
842
|
-
console.log(
|
|
916
|
+
console.log(`⏳ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
|
|
917
|
+
functionsReadinessPromise = waitForHttpServerReady(
|
|
918
|
+
functionsBaseUrl,
|
|
919
|
+
getFunctionsReadinessTimeoutMs(backendLanguage)
|
|
920
|
+
);
|
|
843
921
|
} else if (!hasFunctions) {
|
|
844
922
|
console.log('');
|
|
845
923
|
console.log('ℹ️ functions/ directory not found. Starting Next.js only.');
|
|
@@ -935,8 +1013,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
935
1013
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
936
1014
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
937
1015
|
envLocalPath = path.join(process.cwd(), '.env.local');
|
|
938
|
-
envLocalDefaultUrl =
|
|
939
|
-
const bffTargetUrl =
|
|
1016
|
+
envLocalDefaultUrl = functionsBaseUrl;
|
|
1017
|
+
const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
|
|
940
1018
|
try {
|
|
941
1019
|
if (fs.existsSync(envLocalPath)) {
|
|
942
1020
|
const envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
@@ -953,8 +1031,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
953
1031
|
|
|
954
1032
|
const nextEnv: NodeJS.ProcessEnv = {
|
|
955
1033
|
...process.env,
|
|
956
|
-
BACKEND_FUNCTIONS_BASE_URL:
|
|
957
|
-
FUNCTIONS_BASE_URL:
|
|
1034
|
+
BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
1035
|
+
FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
958
1036
|
};
|
|
959
1037
|
|
|
960
1038
|
const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
|
|
@@ -987,19 +1065,31 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
987
1065
|
process.exit(code || 0);
|
|
988
1066
|
});
|
|
989
1067
|
|
|
1068
|
+
if (functionsReadinessPromise) {
|
|
1069
|
+
functionsReady = await functionsReadinessPromise;
|
|
1070
|
+
console.log('');
|
|
1071
|
+
if (functionsReady) {
|
|
1072
|
+
console.log(`✅ Azure Functions ready (port: ${functionsPort})`);
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log(`⚠️ Azure Functions is still starting: ${functionsBaseUrl}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
990
1078
|
console.log('');
|
|
991
1079
|
console.log('✅ SwallowKit development environment is running!');
|
|
992
1080
|
console.log('');
|
|
993
1081
|
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
994
1082
|
if (hasFunctions && !options.noFunctions) {
|
|
995
|
-
console.log(
|
|
1083
|
+
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
996
1084
|
}
|
|
997
1085
|
if (mockServer) {
|
|
998
|
-
console.log(`🔌 Mock Proxy:
|
|
1086
|
+
console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
|
|
999
1087
|
}
|
|
1000
1088
|
console.log('');
|
|
1001
|
-
if (hasFunctions && !options.noFunctions) {
|
|
1089
|
+
if (hasFunctions && !options.noFunctions && functionsReady) {
|
|
1002
1090
|
console.log('💡 Azure Functions and Next.js BFF are connected');
|
|
1091
|
+
} else if (hasFunctions && !options.noFunctions) {
|
|
1092
|
+
console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
|
|
1003
1093
|
}
|
|
1004
1094
|
if (mockServer) {
|
|
1005
1095
|
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
@@ -1024,12 +1114,13 @@ async function runCommand(
|
|
|
1024
1114
|
args: string[],
|
|
1025
1115
|
cwd: string,
|
|
1026
1116
|
label: string,
|
|
1027
|
-
env?: NodeJS.ProcessEnv
|
|
1117
|
+
env?: NodeJS.ProcessEnv,
|
|
1118
|
+
shell = true
|
|
1028
1119
|
): Promise<void> {
|
|
1029
1120
|
await new Promise<void>((resolve, reject) => {
|
|
1030
1121
|
const child = spawn(command, args, {
|
|
1031
1122
|
cwd,
|
|
1032
|
-
shell
|
|
1123
|
+
shell,
|
|
1033
1124
|
stdio: 'inherit',
|
|
1034
1125
|
env,
|
|
1035
1126
|
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
buildCSharpCodegenToolManifestSource,
|
|
20
20
|
buildPythonCodegenRequirementsSource,
|
|
21
21
|
} from "../../core/scaffold/native-schema-generator";
|
|
22
|
+
import { getProjectLocalUvPaths } from "../../utils/python-uv";
|
|
22
23
|
|
|
23
24
|
interface InitOptions {
|
|
24
25
|
name: string;
|
|
@@ -1128,6 +1129,7 @@ public sealed class GreetFunction
|
|
|
1128
1129
|
|
|
1129
1130
|
function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
|
|
1130
1131
|
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1132
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1131
1133
|
|
|
1132
1134
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1133
1135
|
azure-cosmos>=4.9.0
|
|
@@ -1175,6 +1177,22 @@ app.register_blueprint(greet_bp)
|
|
|
1175
1177
|
`);
|
|
1176
1178
|
}
|
|
1177
1179
|
|
|
1180
|
+
function ensureProjectGitignoreEntry(projectDir: string, entry: string): void {
|
|
1181
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
1182
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
1187
|
+
const lines = current.split(/\r?\n/);
|
|
1188
|
+
if (lines.includes(entry)) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const normalized = current.endsWith('\n') ? current : `${current}\n`;
|
|
1193
|
+
fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1178
1196
|
async function createBffApiRoute(projectDir: string) {
|
|
1179
1197
|
console.log('📦 Creating BFF API route...\n');
|
|
1180
1198
|
|
|
@@ -1414,8 +1432,9 @@ function createReadme(
|
|
|
1414
1432
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1415
1433
|
? '- Azure Functions CRUD endpoints'
|
|
1416
1434
|
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1435
|
+
const pythonUvPaths = getProjectLocalUvPaths(projectDir);
|
|
1417
1436
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1418
|
-
? `\n**Python local dev note**: SwallowKit uses \`
|
|
1437
|
+
? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
|
|
1419
1438
|
: '';
|
|
1420
1439
|
|
|
1421
1440
|
const readme = `# ${projectName}
|
|
@@ -4,33 +4,45 @@ import { spawn, spawnSync } from "child_process";
|
|
|
4
4
|
import { BackendLanguage } from "../../types";
|
|
5
5
|
import { ModelInfo, toKebabCase } from "./model-parser";
|
|
6
6
|
import { generateOpenApiDocument } from "./openapi-generator";
|
|
7
|
+
import {
|
|
8
|
+
buildProjectLocalUvEnv,
|
|
9
|
+
buildProjectLocalUvInstallerEnv,
|
|
10
|
+
buildUvPipInstallArgs,
|
|
11
|
+
buildUvVenvArgs,
|
|
12
|
+
getProjectLocalUvInstallerCommand,
|
|
13
|
+
getProjectLocalUvPaths,
|
|
14
|
+
getPythonProjectRoot,
|
|
15
|
+
} from "../../utils/python-uv";
|
|
7
16
|
|
|
8
17
|
export const NSWAG_CONSOLECORE_VERSION = "14.7.1";
|
|
9
18
|
export const PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
|
|
10
19
|
const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
|
|
11
20
|
|
|
12
|
-
type PythonLauncher = {
|
|
13
|
-
command: string;
|
|
14
|
-
argsPrefix: string[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
21
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
18
22
|
return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
function canRun(command: string, args: string[], cwd: string): boolean {
|
|
25
|
+
function canRun(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): boolean {
|
|
22
26
|
const result = spawnSync(command, args, {
|
|
23
27
|
cwd,
|
|
28
|
+
env,
|
|
24
29
|
stdio: "ignore",
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
return !result.error && result.status === 0;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
async function runCommand(
|
|
35
|
+
async function runCommand(
|
|
36
|
+
command: string,
|
|
37
|
+
args: string[],
|
|
38
|
+
cwd: string,
|
|
39
|
+
errorMessage: string,
|
|
40
|
+
env?: NodeJS.ProcessEnv
|
|
41
|
+
): Promise<void> {
|
|
31
42
|
await new Promise<void>((resolve, reject) => {
|
|
32
43
|
const child = spawn(command, args, {
|
|
33
44
|
cwd,
|
|
45
|
+
env,
|
|
34
46
|
stdio: getMachineAwareStdio(),
|
|
35
47
|
});
|
|
36
48
|
|
|
@@ -611,46 +623,63 @@ function getVirtualEnvPythonPath(venvDir: string): string {
|
|
|
611
623
|
: path.join(venvDir, "bin", "python");
|
|
612
624
|
}
|
|
613
625
|
|
|
614
|
-
function
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
{ command: "py", argsPrefix: ["-3"] },
|
|
618
|
-
{ command: "python3", argsPrefix: [] },
|
|
619
|
-
];
|
|
626
|
+
async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
627
|
+
const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
|
|
628
|
+
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
620
629
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
630
|
+
if (canRun("uv", ["--version"], projectRoot)) {
|
|
631
|
+
return { command: "uv", env: uvEnv };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot)) {
|
|
635
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
625
636
|
}
|
|
626
637
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
638
|
+
const installer = getProjectLocalUvInstallerCommand();
|
|
639
|
+
await runCommand(
|
|
640
|
+
installer.command,
|
|
641
|
+
installer.args,
|
|
642
|
+
projectRoot,
|
|
643
|
+
"Failed to install project-local uv.",
|
|
644
|
+
buildProjectLocalUvInstallerEnv(process.env, projectRoot)
|
|
630
645
|
);
|
|
646
|
+
|
|
647
|
+
if (!(fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot))) {
|
|
648
|
+
throw new Error("Failed to install project-local uv.");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
631
652
|
}
|
|
632
653
|
|
|
633
654
|
async function ensurePythonCodegenEnvironment(functionsRoot: string): Promise<string> {
|
|
634
655
|
const requirementsPath = ensurePythonCodegenProjectFiles(functionsRoot);
|
|
656
|
+
const projectRoot = getPythonProjectRoot(functionsRoot);
|
|
657
|
+
const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
|
|
635
658
|
const venvDir = path.join(functionsRoot, ".codegen-venv");
|
|
636
659
|
const venvPython = getVirtualEnvPythonPath(venvDir);
|
|
637
660
|
|
|
638
|
-
if (!
|
|
639
|
-
const
|
|
661
|
+
if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
|
|
662
|
+
const venvArgs = buildUvVenvArgs(venvDir);
|
|
663
|
+
if (fs.existsSync(venvDir)) {
|
|
664
|
+
venvArgs.push("--clear");
|
|
665
|
+
}
|
|
666
|
+
|
|
640
667
|
await runCommand(
|
|
641
|
-
|
|
642
|
-
|
|
668
|
+
uvCommand,
|
|
669
|
+
venvArgs,
|
|
643
670
|
functionsRoot,
|
|
644
|
-
"Failed to create the Python schema code generation virtual environment."
|
|
671
|
+
"Failed to create the Python schema code generation virtual environment.",
|
|
672
|
+
uvEnv
|
|
645
673
|
);
|
|
646
674
|
}
|
|
647
675
|
|
|
648
|
-
if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot)) {
|
|
676
|
+
if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
|
|
649
677
|
await runCommand(
|
|
650
|
-
|
|
651
|
-
|
|
678
|
+
uvCommand,
|
|
679
|
+
buildUvPipInstallArgs(venvPython, requirementsPath),
|
|
652
680
|
functionsRoot,
|
|
653
|
-
"Failed to install Python schema generation dependencies."
|
|
681
|
+
"Failed to install Python schema generation dependencies.",
|
|
682
|
+
uvEnv
|
|
654
683
|
);
|
|
655
684
|
}
|
|
656
685
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
|
|
3
|
+
export const SWALLOWKIT_PYTHON_VERSION = "3.11";
|
|
4
|
+
|
|
5
|
+
export interface ProjectLocalUvPaths {
|
|
6
|
+
stateDir: string;
|
|
7
|
+
cacheDir: string;
|
|
8
|
+
pythonInstallDir: string;
|
|
9
|
+
toolDir: string;
|
|
10
|
+
toolBinDir: string;
|
|
11
|
+
localUvInstallDir: string;
|
|
12
|
+
localUvExecutable: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getPythonProjectRoot(functionsDir: string): string {
|
|
16
|
+
return path.dirname(functionsDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getProjectLocalUvPaths(projectRoot: string): ProjectLocalUvPaths {
|
|
20
|
+
const stateDir = path.join(projectRoot, ".uv");
|
|
21
|
+
const localUvInstallDir = path.join(stateDir, "bin");
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
stateDir,
|
|
25
|
+
cacheDir: path.join(stateDir, "cache"),
|
|
26
|
+
pythonInstallDir: path.join(stateDir, "python"),
|
|
27
|
+
toolDir: path.join(stateDir, "tools"),
|
|
28
|
+
toolBinDir: path.join(stateDir, "tools", "bin"),
|
|
29
|
+
localUvInstallDir,
|
|
30
|
+
localUvExecutable: process.platform === "win32"
|
|
31
|
+
? path.join(localUvInstallDir, "uv.exe")
|
|
32
|
+
: path.join(localUvInstallDir, "uv"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildProjectLocalUvEnv(
|
|
37
|
+
baseEnv: NodeJS.ProcessEnv,
|
|
38
|
+
projectRoot: string
|
|
39
|
+
): NodeJS.ProcessEnv {
|
|
40
|
+
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
41
|
+
const env: NodeJS.ProcessEnv = {
|
|
42
|
+
...baseEnv,
|
|
43
|
+
UV_CACHE_DIR: uvPaths.cacheDir,
|
|
44
|
+
UV_PYTHON_INSTALL_DIR: uvPaths.pythonInstallDir,
|
|
45
|
+
UV_TOOL_DIR: uvPaths.toolDir,
|
|
46
|
+
UV_TOOL_BIN_DIR: uvPaths.toolBinDir,
|
|
47
|
+
UV_PYTHON_PREFERENCE: "only-managed",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (process.platform === "win32") {
|
|
51
|
+
env.UV_PYTHON_NO_REGISTRY = "true";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return env;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildProjectLocalUvInstallerEnv(
|
|
58
|
+
baseEnv: NodeJS.ProcessEnv,
|
|
59
|
+
projectRoot: string
|
|
60
|
+
): NodeJS.ProcessEnv {
|
|
61
|
+
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...buildProjectLocalUvEnv(baseEnv, projectRoot),
|
|
65
|
+
UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
|
|
66
|
+
UV_NO_MODIFY_PATH: "1",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getProjectLocalUvInstallerCommand(): { command: string; args: string[] } {
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
return {
|
|
73
|
+
command: "powershell",
|
|
74
|
+
args: [
|
|
75
|
+
"-NoProfile",
|
|
76
|
+
"-ExecutionPolicy",
|
|
77
|
+
"Bypass",
|
|
78
|
+
"-Command",
|
|
79
|
+
"$ProgressPreference = 'SilentlyContinue'; irm https://astral.sh/uv/install.ps1 | iex",
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
command: "sh",
|
|
86
|
+
args: ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildUvVenvArgs(venvDir: string, pythonVersion = SWALLOWKIT_PYTHON_VERSION): string[] {
|
|
91
|
+
return ["venv", venvDir, "--python", pythonVersion];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildUvPipInstallArgs(pythonExecutable: string, requirementsPath: string): string[] {
|
|
95
|
+
return ["pip", "install", "--python", pythonExecutable, "-r", requirementsPath];
|
|
96
|
+
}
|