swallowkit 1.0.0-beta.22 ā 1.0.0-beta.24
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 +9 -4
- package/README.md +9 -4
- package/dist/cli/commands/dev.d.ts +14 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +187 -54
- 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 +33 -18
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +0 -3
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +3 -172
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/core/project/validation.js +2 -2
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +5 -6
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
- package/dist/core/scaffold/native-schema-generator.js +677 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- 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 +112 -0
- package/dist/utils/python-uv.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/dev.test.ts +95 -0
- package/src/__tests__/model-parser.test.ts +44 -64
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +54 -26
- package/src/cli/commands/dev.ts +258 -74
- package/src/cli/commands/init.ts +45 -19
- package/src/cli/commands/scaffold.ts +3 -213
- package/src/core/project/validation.ts +2 -2
- package/src/core/scaffold/model-parser.ts +7 -7
- package/src/core/scaffold/native-schema-generator.ts +798 -0
- package/src/utils/python-uv.ts +97 -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 {
|
|
@@ -27,6 +38,15 @@ type ParsedDevActionOptions = DevOptions & {
|
|
|
27
38
|
functions?: boolean;
|
|
28
39
|
};
|
|
29
40
|
|
|
41
|
+
interface FunctionsCoreToolsCommand {
|
|
42
|
+
command: string;
|
|
43
|
+
argsPrefix: string[];
|
|
44
|
+
label: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
|
|
48
|
+
const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
|
|
49
|
+
|
|
30
50
|
function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions {
|
|
31
51
|
return {
|
|
32
52
|
...options,
|
|
@@ -38,11 +58,89 @@ export function buildFunctionsStartArgs(functionsPort: string): string[] {
|
|
|
38
58
|
return ['start', '--port', functionsPort];
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
export function parseCoreToolsVersion(output: string): string | null {
|
|
62
|
+
const match = output.match(/\d+\.\d+\.\d+/);
|
|
63
|
+
return match ? match[0] : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function compareVersionNumbers(left: string, right: string): number {
|
|
67
|
+
const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
|
|
68
|
+
const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
|
|
69
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < length; index += 1) {
|
|
72
|
+
const leftPart = leftParts[index] ?? 0;
|
|
73
|
+
const rightPart = rightParts[index] ?? 0;
|
|
74
|
+
|
|
75
|
+
if (leftPart !== rightPart) {
|
|
76
|
+
return leftPart - rightPart;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildFunctionsCoreToolsCommand(
|
|
84
|
+
backendLanguage: BackendLanguage,
|
|
85
|
+
installedVersion: string | null
|
|
86
|
+
): FunctionsCoreToolsCommand {
|
|
87
|
+
if (
|
|
88
|
+
backendLanguage === 'csharp' &&
|
|
89
|
+
(!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)
|
|
90
|
+
) {
|
|
91
|
+
const reason = installedVersion
|
|
92
|
+
? `installed func ${installedVersion} is too old for C# isolated`
|
|
93
|
+
: 'func is not installed';
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
command: 'npm',
|
|
97
|
+
argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
|
|
98
|
+
label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
command: 'func',
|
|
104
|
+
argsPrefix: [],
|
|
105
|
+
label: installedVersion ? `func ${installedVersion}` : 'func',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
41
109
|
export function buildNextDevArgs(pm: string, port: string): string[] {
|
|
42
110
|
const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
|
|
43
111
|
return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
|
|
44
112
|
}
|
|
45
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
|
+
|
|
46
144
|
export function getPythonVirtualEnvPaths(functionsDir: string): {
|
|
47
145
|
venvDir: string;
|
|
48
146
|
binDir: string;
|
|
@@ -59,6 +157,13 @@ export function getPythonVirtualEnvPaths(functionsDir: string): {
|
|
|
59
157
|
return { venvDir, binDir, pythonExecutable };
|
|
60
158
|
}
|
|
61
159
|
|
|
160
|
+
export function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[] {
|
|
161
|
+
return [
|
|
162
|
+
path.join(functionsDir, 'bin'),
|
|
163
|
+
path.join(functionsDir, 'obj'),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
62
167
|
export function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv {
|
|
63
168
|
const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
64
169
|
const pathKey = getPathEnvKey(baseEnv);
|
|
@@ -93,10 +198,20 @@ async function checkCoreTools(): Promise<boolean> {
|
|
|
93
198
|
return checkCommand('func', ['--version']);
|
|
94
199
|
}
|
|
95
200
|
|
|
96
|
-
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> {
|
|
97
210
|
return new Promise((resolve) => {
|
|
98
211
|
const checkProcess = spawn(command, args, {
|
|
99
|
-
|
|
212
|
+
cwd: options?.cwd,
|
|
213
|
+
env: options?.env ?? process.env,
|
|
214
|
+
shell: options?.shell ?? true,
|
|
100
215
|
stdio: 'pipe',
|
|
101
216
|
});
|
|
102
217
|
|
|
@@ -110,26 +225,34 @@ async function checkCommand(command: string, args: string[] = ['--version']): Pr
|
|
|
110
225
|
});
|
|
111
226
|
}
|
|
112
227
|
|
|
113
|
-
async function
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
for (const candidate of candidates) {
|
|
125
|
-
if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
|
|
126
|
-
return candidate;
|
|
127
|
-
}
|
|
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 };
|
|
128
238
|
}
|
|
129
239
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
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 };
|
|
133
256
|
}
|
|
134
257
|
|
|
135
258
|
async function getCommandPath(command: string): Promise<string | null> {
|
|
@@ -143,6 +266,18 @@ async function getCommandPath(command: string): Promise<string | null> {
|
|
|
143
266
|
return firstLine || null;
|
|
144
267
|
}
|
|
145
268
|
|
|
269
|
+
async function resolveInstalledCoreToolsVersion(): Promise<string | null> {
|
|
270
|
+
if (!(await checkCoreTools())) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
146
281
|
async function captureCommandOutput(
|
|
147
282
|
command: string,
|
|
148
283
|
args: string[],
|
|
@@ -180,6 +315,41 @@ async function captureCommandOutput(
|
|
|
180
315
|
});
|
|
181
316
|
}
|
|
182
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
|
+
|
|
183
353
|
async function resolvePythonRuntimeDetails(
|
|
184
354
|
functionsDir: string,
|
|
185
355
|
env: NodeJS.ProcessEnv
|
|
@@ -255,47 +425,36 @@ async function bridgePythonCoreToolsForWindowsArm64(
|
|
|
255
425
|
}
|
|
256
426
|
|
|
257
427
|
async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const bootstrap = await resolvePythonBootstrapCommand();
|
|
267
|
-
console.log(`š¦ Creating Python virtual environment with ${bootstrap.label}...`);
|
|
268
|
-
await runCommand(
|
|
269
|
-
bootstrap.command,
|
|
270
|
-
[...bootstrap.argsPrefix, '-m', 'venv', '.venv'],
|
|
271
|
-
functionsDir,
|
|
272
|
-
'python virtual environment setup'
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
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
|
+
});
|
|
276
436
|
|
|
277
|
-
|
|
278
|
-
|
|
437
|
+
if (!hasUsableVirtualEnv) {
|
|
438
|
+
const venvArgs = buildUvVenvArgs('.venv');
|
|
439
|
+
if (fs.existsSync(venvDir)) {
|
|
440
|
+
venvArgs.push('--clear');
|
|
441
|
+
}
|
|
279
442
|
|
|
280
|
-
|
|
281
|
-
await runCommand(
|
|
282
|
-
'uv',
|
|
283
|
-
['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'],
|
|
284
|
-
functionsDir,
|
|
285
|
-
'python dependency installation',
|
|
286
|
-
pythonEnv
|
|
287
|
-
);
|
|
288
|
-
} else {
|
|
289
|
-
await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
|
|
290
|
-
await runCommand(
|
|
291
|
-
'python',
|
|
292
|
-
['-m', 'pip', 'install', '-r', 'requirements.txt'],
|
|
293
|
-
functionsDir,
|
|
294
|
-
'python dependency installation',
|
|
295
|
-
pythonEnv
|
|
296
|
-
);
|
|
443
|
+
console.log('š¦ Creating Python virtual environment with uv...');
|
|
444
|
+
await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
|
|
297
445
|
}
|
|
298
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);
|
|
299
458
|
return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
|
|
300
459
|
}
|
|
301
460
|
|
|
@@ -338,7 +497,7 @@ export function buildDevCommand(
|
|
|
338
497
|
.option('--host <host>', 'Host name', 'localhost')
|
|
339
498
|
.option('--open', 'Open browser automatically', false)
|
|
340
499
|
.option('--verbose', 'Show verbose logs', false)
|
|
341
|
-
.option('--no-functions', 'Skip Azure Functions startup'
|
|
500
|
+
.option('--no-functions', 'Skip Azure Functions startup')
|
|
342
501
|
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
343
502
|
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
344
503
|
.action(async (options: ParsedDevActionOptions) => {
|
|
@@ -480,6 +639,9 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
480
639
|
let mockServer: ConnectorMockServer | null = null;
|
|
481
640
|
let envLocalPath = '';
|
|
482
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;
|
|
483
645
|
|
|
484
646
|
// Cleanup processes on Ctrl+C
|
|
485
647
|
process.on('SIGINT', async () => {
|
|
@@ -532,11 +694,14 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
532
694
|
const functionsDir = path.join(process.cwd(), 'functions');
|
|
533
695
|
const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
|
|
534
696
|
|
|
697
|
+
let functionsCoreToolsCommand: FunctionsCoreToolsCommand | null = null;
|
|
698
|
+
let installedCoreToolsVersion: string | null = null;
|
|
699
|
+
|
|
535
700
|
if (hasFunctions && !options.noFunctions) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (!
|
|
701
|
+
installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
|
|
702
|
+
functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
703
|
+
|
|
704
|
+
if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
|
|
540
705
|
console.log('');
|
|
541
706
|
console.log('ā ļø Azure Functions Core Tools not found.');
|
|
542
707
|
console.log('');
|
|
@@ -585,6 +750,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
585
750
|
// Skip Azure Functions startup
|
|
586
751
|
options.noFunctions = true;
|
|
587
752
|
}
|
|
753
|
+
} else if (functionsCoreToolsCommand.command !== 'func') {
|
|
754
|
+
console.log(`ā¹ļø Using ${functionsCoreToolsCommand.label}.`);
|
|
588
755
|
}
|
|
589
756
|
|
|
590
757
|
if (!options.noFunctions) {
|
|
@@ -631,8 +798,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
631
798
|
await runCommand(pm, ['install'], functionsDir, `${pm} install`);
|
|
632
799
|
}
|
|
633
800
|
} else if (backendLanguage === 'csharp') {
|
|
634
|
-
console.log('
|
|
635
|
-
await runCommand('dotnet', ['build'], functionsDir, 'dotnet build');
|
|
801
|
+
console.log('ā¹ļø C# Azure Functions can take longer on cold start while the worker builds.');
|
|
636
802
|
} else {
|
|
637
803
|
functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
|
|
638
804
|
}
|
|
@@ -685,7 +851,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
685
851
|
}
|
|
686
852
|
|
|
687
853
|
// Azure Functions ćčµ·å
|
|
688
|
-
const
|
|
854
|
+
const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
855
|
+
const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
|
|
689
856
|
cwd: functionsDir,
|
|
690
857
|
shell: true,
|
|
691
858
|
stdio: 'pipe', // Always pipe to capture output
|
|
@@ -746,7 +913,11 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
746
913
|
}
|
|
747
914
|
});
|
|
748
915
|
|
|
749
|
-
console.log(
|
|
916
|
+
console.log(`ā³ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
|
|
917
|
+
functionsReadinessPromise = waitForHttpServerReady(
|
|
918
|
+
functionsBaseUrl,
|
|
919
|
+
getFunctionsReadinessTimeoutMs(backendLanguage)
|
|
920
|
+
);
|
|
750
921
|
} else if (!hasFunctions) {
|
|
751
922
|
console.log('');
|
|
752
923
|
console.log('ā¹ļø functions/ directory not found. Starting Next.js only.');
|
|
@@ -842,8 +1013,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
842
1013
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
843
1014
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
844
1015
|
envLocalPath = path.join(process.cwd(), '.env.local');
|
|
845
|
-
envLocalDefaultUrl =
|
|
846
|
-
const bffTargetUrl =
|
|
1016
|
+
envLocalDefaultUrl = functionsBaseUrl;
|
|
1017
|
+
const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
|
|
847
1018
|
try {
|
|
848
1019
|
if (fs.existsSync(envLocalPath)) {
|
|
849
1020
|
const envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
@@ -860,8 +1031,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
860
1031
|
|
|
861
1032
|
const nextEnv: NodeJS.ProcessEnv = {
|
|
862
1033
|
...process.env,
|
|
863
|
-
BACKEND_FUNCTIONS_BASE_URL:
|
|
864
|
-
FUNCTIONS_BASE_URL:
|
|
1034
|
+
BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
1035
|
+
FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
865
1036
|
};
|
|
866
1037
|
|
|
867
1038
|
const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
|
|
@@ -894,19 +1065,31 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
894
1065
|
process.exit(code || 0);
|
|
895
1066
|
});
|
|
896
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
|
+
|
|
897
1078
|
console.log('');
|
|
898
1079
|
console.log('ā
SwallowKit development environment is running!');
|
|
899
1080
|
console.log('');
|
|
900
1081
|
console.log(`š± Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
901
1082
|
if (hasFunctions && !options.noFunctions) {
|
|
902
|
-
console.log(
|
|
1083
|
+
console.log(`${functionsReady ? 'ā” Azure Functions' : 'ā³ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
903
1084
|
}
|
|
904
1085
|
if (mockServer) {
|
|
905
|
-
console.log(`š Mock Proxy:
|
|
1086
|
+
console.log(`š Mock Proxy: ${bffTargetUrl} (BFF ā here)`);
|
|
906
1087
|
}
|
|
907
1088
|
console.log('');
|
|
908
|
-
if (hasFunctions && !options.noFunctions) {
|
|
1089
|
+
if (hasFunctions && !options.noFunctions && functionsReady) {
|
|
909
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.');
|
|
910
1093
|
}
|
|
911
1094
|
if (mockServer) {
|
|
912
1095
|
console.log('š” Connector models served from mock server (Zod-generated data)');
|
|
@@ -931,12 +1114,13 @@ async function runCommand(
|
|
|
931
1114
|
args: string[],
|
|
932
1115
|
cwd: string,
|
|
933
1116
|
label: string,
|
|
934
|
-
env?: NodeJS.ProcessEnv
|
|
1117
|
+
env?: NodeJS.ProcessEnv,
|
|
1118
|
+
shell = true
|
|
935
1119
|
): Promise<void> {
|
|
936
1120
|
await new Promise<void>((resolve, reject) => {
|
|
937
1121
|
const child = spawn(command, args, {
|
|
938
1122
|
cwd,
|
|
939
|
-
shell
|
|
1123
|
+
shell,
|
|
940
1124
|
stdio: 'inherit',
|
|
941
1125
|
env,
|
|
942
1126
|
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
getFunctionsStartScript,
|
|
16
16
|
} from "../../utils/package-manager";
|
|
17
17
|
import { syncProjectManifest } from "../../core/project/manifest";
|
|
18
|
+
import {
|
|
19
|
+
buildCSharpCodegenToolManifestSource,
|
|
20
|
+
buildPythonCodegenRequirementsSource,
|
|
21
|
+
} from "../../core/scaffold/native-schema-generator";
|
|
22
|
+
import { getProjectLocalUvPaths } from "../../utils/python-uv";
|
|
18
23
|
|
|
19
24
|
interface InitOptions {
|
|
20
25
|
name: string;
|
|
@@ -473,13 +478,6 @@ async function addSwallowKitFiles(
|
|
|
473
478
|
...buildGeneratedProjectDependencies(projectName),
|
|
474
479
|
};
|
|
475
480
|
|
|
476
|
-
if (backendLanguage !== "typescript") {
|
|
477
|
-
packageJson.devDependencies = {
|
|
478
|
-
...packageJson.devDependencies,
|
|
479
|
-
'@openapitools/openapi-generator-cli': '^2.21.0',
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
481
|
packageJson.scripts = {
|
|
484
482
|
...packageJson.scripts,
|
|
485
483
|
'build': getBuildScript(pm),
|
|
@@ -955,8 +953,9 @@ tsconfig.json
|
|
|
955
953
|
!dist/**/*.js
|
|
956
954
|
`);
|
|
957
955
|
} else if (backendLanguage === 'python') {
|
|
958
|
-
gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
|
|
956
|
+
gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
|
|
959
957
|
fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
|
|
958
|
+
.codegen-venv
|
|
960
959
|
__pycache__
|
|
961
960
|
.pytest_cache
|
|
962
961
|
.mypy_cache
|
|
@@ -1083,6 +1082,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
|
|
|
1083
1082
|
const csprojName = `${projectPascal}.Functions.csproj`;
|
|
1084
1083
|
|
|
1085
1084
|
fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
|
|
1085
|
+
fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
|
|
1086
|
+
fs.writeFileSync(
|
|
1087
|
+
path.join(functionsDir, '.config', 'dotnet-tools.json'),
|
|
1088
|
+
buildCSharpCodegenToolManifestSource()
|
|
1089
|
+
);
|
|
1086
1090
|
|
|
1087
1091
|
fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
|
|
1088
1092
|
|
|
@@ -1125,11 +1129,16 @@ public sealed class GreetFunction
|
|
|
1125
1129
|
|
|
1126
1130
|
function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
|
|
1127
1131
|
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1132
|
+
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1128
1133
|
|
|
1129
1134
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
1130
1135
|
azure-cosmos>=4.9.0
|
|
1131
1136
|
azure-identity>=1.19.0
|
|
1132
1137
|
`);
|
|
1138
|
+
fs.writeFileSync(
|
|
1139
|
+
path.join(functionsDir, 'requirements.codegen.txt'),
|
|
1140
|
+
buildPythonCodegenRequirementsSource()
|
|
1141
|
+
);
|
|
1133
1142
|
|
|
1134
1143
|
const blueprintsDir = path.join(functionsDir, 'blueprints');
|
|
1135
1144
|
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
@@ -1168,6 +1177,22 @@ app.register_blueprint(greet_bp)
|
|
|
1168
1177
|
`);
|
|
1169
1178
|
}
|
|
1170
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
|
+
|
|
1171
1196
|
async function createBffApiRoute(projectDir: string) {
|
|
1172
1197
|
console.log('š¦ Creating BFF API route...\n');
|
|
1173
1198
|
|
|
@@ -1398,17 +1423,18 @@ function createReadme(
|
|
|
1398
1423
|
const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
|
|
1399
1424
|
const schemaBridgeDescription = backendLanguage === 'typescript'
|
|
1400
1425
|
? 'Zod (shared between frontend and backend)'
|
|
1401
|
-
: `Zod + OpenAPI
|
|
1426
|
+
: `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
|
|
1402
1427
|
const functionsTree = backendLanguage === 'typescript'
|
|
1403
1428
|
? `ā āāā src/\nā āāā greet.ts # Sample function`
|
|
1404
1429
|
: backendLanguage === 'csharp'
|
|
1405
|
-
? `ā āāā Crud/\nā ā āāā GreetFunction.cs\nā āāā generated/ #
|
|
1406
|
-
: `ā āāā blueprints/\nā ā āāā greet.py\nā āāā generated/ #
|
|
1430
|
+
? `ā āāā Crud/\nā ā āāā GreetFunction.cs\nā āāā generated/ # Native-generated C# schema assets`
|
|
1431
|
+
: `ā āāā blueprints/\nā ā āāā greet.py\nā āāā generated/ # Native-generated Python schema assets`;
|
|
1407
1432
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1408
1433
|
? '- Azure Functions CRUD endpoints'
|
|
1409
|
-
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI
|
|
1434
|
+
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1435
|
+
const pythonUvPaths = getProjectLocalUvPaths(projectDir);
|
|
1410
1436
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1411
|
-
? `\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`
|
|
1412
1438
|
: '';
|
|
1413
1439
|
|
|
1414
1440
|
const readme = `# ${projectName}
|
|
@@ -1626,14 +1652,14 @@ function createAiAgentFiles(projectDir: string, projectName: string, backendLang
|
|
|
1626
1652
|
const functionsStructureLine = backendLanguage === 'typescript'
|
|
1627
1653
|
? `ā āāā src/ # HTTP trigger handlers with Cosmos DB bindings`
|
|
1628
1654
|
: backendLanguage === 'csharp'
|
|
1629
|
-
? `ā āāā Crud/ # C# HTTP trigger handlers\nā āāā generated/ #
|
|
1630
|
-
: `ā āāā blueprints/ # Python HTTP trigger handlers\nā āāā generated/ #
|
|
1655
|
+
? `ā āāā Crud/ # C# HTTP trigger handlers\nā āāā generated/ # Native-generated C# schema assets`
|
|
1656
|
+
: `ā āāā blueprints/ # Python HTTP trigger handlers\nā āāā generated/ # Native-generated Python schema assets`;
|
|
1631
1657
|
const backendSchemaNote = backendLanguage === 'typescript'
|
|
1632
1658
|
? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
|
|
1633
|
-
: `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\`
|
|
1659
|
+
: `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` with native ${backendLanguageLabel} tooling.`;
|
|
1634
1660
|
const backendRulesNote = backendLanguage === 'typescript'
|
|
1635
1661
|
? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` ā never trust client-sent values for these fields.`
|
|
1636
|
-
: `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the generated
|
|
1662
|
+
: `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the native-generated schema assets in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
|
|
1637
1663
|
|
|
1638
1664
|
// āā 1. AGENTS.md (Codex / generic agents) āāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1639
1665
|
|
|
@@ -1748,7 +1774,7 @@ Key rules:
|
|
|
1748
1774
|
|
|
1749
1775
|
- ${backendRulesNote}
|
|
1750
1776
|
|
|
1751
|
-
${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and \`functions/generated/\` in sync.`}
|
|
1777
|
+
${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and the native schema assets under \`functions/generated/\` in sync.`}
|
|
1752
1778
|
|
|
1753
1779
|
${backendLanguage === 'typescript' ? `\`\`\`typescript
|
|
1754
1780
|
// functions/src/{model}.ts
|
|
@@ -2018,7 +2044,7 @@ Files in \`functions/\` contain all business logic and data access for this appl
|
|
|
2018
2044
|
|
|
2019
2045
|
- Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
|
|
2020
2046
|
- For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
|
|
2021
|
-
- For C#/Python backends, consume the generated
|
|
2047
|
+
- For C#/Python backends, consume the native-generated assets in \`functions/generated/\`.
|
|
2022
2048
|
- Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
|
|
2023
2049
|
- Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key defaults to \`/id\` but can be customized per model.
|
|
2024
2050
|
|