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/README.ja.md
CHANGED
|
@@ -22,8 +22,8 @@ Zod スキーマから自動的に CRUD 操作を生成する Scaffold 機能を
|
|
|
22
22
|
- **🔄 Zod スキーマ共有** - フロントエンド、BFF、Azure Functions、Cosmos DB をまたいで Zod を唯一のソースとして維持
|
|
23
23
|
- **⚡ CRUD コード生成** - `swallowkit scaffold` で Azure Functions + Next.js コードを自動生成
|
|
24
24
|
- **🌐 Functions バックエンド多言語対応** - `init` 時に Azure Functions の言語として TypeScript、C#、Python を選択可能
|
|
25
|
-
- **🧬 OpenAPI
|
|
26
|
-
- **🛡️ 契約安全性** - 共有 Zod
|
|
25
|
+
- **🧬 OpenAPI 出力 + ネイティブコード生成** - C#/Python バックエンドでは `scaffold` が Zod から OpenAPI を出力し、各言語のネイティブツールでスキーマ資産を生成
|
|
26
|
+
- **🛡️ 契約安全性** - 共有 Zod またはネイティブ生成されたモデルにより、フロント/BFF とバックエンドの契約を整合
|
|
27
27
|
- **🎯 BFF パターン** - Next.js API Routes が BFF レイヤーとして機能、自動検証・リソース名推論
|
|
28
28
|
- **☁️ Azure 最適化** - Static Web Apps + Functions + Cosmos DB で最小コスト構成
|
|
29
29
|
- **🚀 簡単デプロイ** - Bicep IaC + CI/CD ワークフローを自動生成
|
|
@@ -132,7 +132,7 @@ pnpm dlx swallowkit scaffold shared/models/todo.ts
|
|
|
132
132
|
- ✅ Next.js BFF API Routes (自動検証・リソース名推論)
|
|
133
133
|
- ✅ React コンポーネント (型安全なフォーム)
|
|
134
134
|
|
|
135
|
-
`init` で `csharp` または `python` を選んだ場合、`swallowkit scaffold` はあわせて `functions/openapi/` に OpenAPI ドキュメントを出力し、`functions/generated/`
|
|
135
|
+
`init` で `csharp` または `python` を選んだ場合、`swallowkit scaffold` はあわせて `functions/openapi/` に OpenAPI ドキュメントを出力し、`functions/generated/` にネイティブ生成された各言語向けスキーマ資産を生成します。
|
|
136
136
|
|
|
137
137
|
### 4. 開発サーバー起動
|
|
138
138
|
|
|
@@ -145,6 +145,11 @@ pnpm dlx swallowkit dev
|
|
|
145
145
|
- Next.js: http://localhost:3000
|
|
146
146
|
- Azure Functions: http://localhost:7071
|
|
147
147
|
|
|
148
|
+
バックエンド言語ごとの補足:
|
|
149
|
+
|
|
150
|
+
- **Python バックエンド**: `swallowkit dev` はローカルの Python 実行環境管理に **uv** を使います。プロジェクト内の `.uv/bin` に `uv` 本体を導入または再利用し、`.uv/python` に uv 管理の Python を保持し、Functions 本体用に `functions/.venv`、Python スキーマ生成用に `functions/.codegen-venv` を作成します。
|
|
151
|
+
- **C# バックエンド**: Azure Functions isolated worker はコールドスタート時にワーカービルドが入るため、応答開始まで少し時間がかかることがあります。`swallowkit dev` は Functions ホストが HTTP 応答できる状態になるまで待ってから、バックエンド URL を ready として表示します。
|
|
152
|
+
|
|
148
153
|
### 5. フロントエンドから使用
|
|
149
154
|
|
|
150
155
|
```typescript
|
|
@@ -255,7 +260,7 @@ MCP server は explicit な Tool だけを公開する薄い adapter で、実
|
|
|
255
260
|
**重要なパターン:**
|
|
256
261
|
- **BFF (Backend For Frontend)**: Next.js API Routes が Azure Functions へのプロキシ
|
|
257
262
|
- **共有スキーマ**: `shared/models/` の Zod スキーマを唯一のソースとして扱う
|
|
258
|
-
- **C#/Python 向け OpenAPI
|
|
263
|
+
- **C#/Python 向け OpenAPI 出力**: TypeScript 以外の Functions は `functions/generated/` のネイティブ生成資産を利用
|
|
259
264
|
- **外部コネクタ**: MySQL・PostgreSQL・REST API — scaffold 生成の Functions で同じ BFF パターンを維持
|
|
260
265
|
- **契約安全性**: 共有 Zod または生成モデルで BFF とバックエンドの整合を保つ
|
|
261
266
|
- **マネージド ID**: サービス間の安全な接続(接続文字列不要)
|
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ Featuring Scaffold functionality to automatically generate CRUD operations from
|
|
|
24
24
|
- **🔄 Zod Schema Sharing** - Keep Zod as the source of truth across frontend, BFF, Azure Functions, and Cosmos DB
|
|
25
25
|
- **⚡ CRUD Code Generation** - Auto-generate Azure Functions + Next.js code with `swallowkit scaffold`
|
|
26
26
|
- **🌐 Multi-language Functions Backends** - Choose TypeScript, C#, or Python for Azure Functions during `init`
|
|
27
|
-
- **🧬 OpenAPI
|
|
28
|
-
- **🛡️ Contract Safety** - Keep frontend/BFF contracts aligned with backend implementations through shared Zod or generated
|
|
27
|
+
- **🧬 OpenAPI Export + Native Codegen** - For C#/Python backends, `scaffold` exports OpenAPI from Zod and generates backend schema assets with native language tooling
|
|
28
|
+
- **🛡️ Contract Safety** - Keep frontend/BFF contracts aligned with backend implementations through shared Zod or native-generated backend models
|
|
29
29
|
- **🎯 BFF Pattern** - Next.js API Routes as BFF layer with auto-validation and resource inference
|
|
30
30
|
- **☁️ Azure Optimized** - Minimal-cost architecture with Static Web Apps + Functions + Cosmos DB
|
|
31
31
|
- **🚀 Easy Deployment** - Auto-generated Bicep IaC + CI/CD workflows
|
|
@@ -133,7 +133,7 @@ This auto-generates:
|
|
|
133
133
|
- ✅ Next.js BFF API Routes (auto-validation + resource inference)
|
|
134
134
|
- ✅ React Components (type-safe forms)
|
|
135
135
|
|
|
136
|
-
If you selected `csharp` or `python` at `init` time, `swallowkit scaffold` also writes an OpenAPI document under `functions/openapi/` and generates backend schema assets under `functions/generated/`.
|
|
136
|
+
If you selected `csharp` or `python` at `init` time, `swallowkit scaffold` also writes an OpenAPI document under `functions/openapi/` and generates native backend schema assets under `functions/generated/`.
|
|
137
137
|
|
|
138
138
|
### 4. Start Development Server
|
|
139
139
|
|
|
@@ -146,6 +146,11 @@ pnpm dlx swallowkit dev
|
|
|
146
146
|
- Next.js: http://localhost:3000
|
|
147
147
|
- Azure Functions: http://localhost:7071
|
|
148
148
|
|
|
149
|
+
Backend-specific notes:
|
|
150
|
+
|
|
151
|
+
- **Python backends**: `swallowkit dev` uses **uv** for local runtime management. It installs or reuses a project-local `uv` binary under `.uv/bin`, keeps uv-managed Python under `.uv/python`, creates `functions/.venv` for the Functions app, and creates `functions/.codegen-venv` for Python schema generation.
|
|
152
|
+
- **C# backends**: Azure Functions isolated worker can take longer to answer on cold start while the worker build completes. `swallowkit dev` waits for the Functions host to start responding before it prints the backend URL as ready.
|
|
153
|
+
|
|
149
154
|
If you want to replace Cosmos DB Emulator data before startup, generate an environment template and then launch `dev` with that environment:
|
|
150
155
|
|
|
151
156
|
```bash
|
|
@@ -312,7 +317,7 @@ The MCP server is intentionally a thin adapter with explicit tools only. It dele
|
|
|
312
317
|
**Key Patterns:**
|
|
313
318
|
- **BFF (Backend For Frontend)**: Next.js API Routes proxy to Azure Functions
|
|
314
319
|
- **Shared Schemas**: Zod schemas stay in `shared/models/` as the source of truth
|
|
315
|
-
- **OpenAPI
|
|
320
|
+
- **OpenAPI Export for C#/Python**: Non-TypeScript Functions consume native-generated assets under `functions/generated/`
|
|
316
321
|
- **External Connectors**: MySQL, PostgreSQL, REST APIs — scaffold-generated Functions with the same BFF pattern
|
|
317
322
|
- **Contract Safety**: BFF and backend stay aligned through shared Zod or generated backend models
|
|
318
323
|
- **Managed Identity**: Secure service connections (no connection strings)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { BackendLanguage } from '../../types';
|
|
2
3
|
export interface DevOptions {
|
|
3
4
|
port?: string;
|
|
4
5
|
functionsPort?: string;
|
|
@@ -9,14 +10,27 @@ export interface DevOptions {
|
|
|
9
10
|
seedEnv?: string;
|
|
10
11
|
mockConnectors?: boolean;
|
|
11
12
|
}
|
|
13
|
+
interface FunctionsCoreToolsCommand {
|
|
14
|
+
command: string;
|
|
15
|
+
argsPrefix: string[];
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
12
18
|
export declare function buildFunctionsStartArgs(functionsPort: string): string[];
|
|
19
|
+
export declare function parseCoreToolsVersion(output: string): string | null;
|
|
20
|
+
export declare function compareVersionNumbers(left: string, right: string): number;
|
|
21
|
+
export declare function buildFunctionsCoreToolsCommand(backendLanguage: BackendLanguage, installedVersion: string | null): FunctionsCoreToolsCommand;
|
|
13
22
|
export declare function buildNextDevArgs(pm: string, port: string): string[];
|
|
23
|
+
export declare function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string;
|
|
24
|
+
export declare function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number;
|
|
25
|
+
export declare function waitForHttpServerReady(url: string, timeoutMs?: number, intervalMs?: number): Promise<boolean>;
|
|
14
26
|
export declare function getPythonVirtualEnvPaths(functionsDir: string): {
|
|
15
27
|
venvDir: string;
|
|
16
28
|
binDir: string;
|
|
17
29
|
pythonExecutable: string;
|
|
18
30
|
};
|
|
31
|
+
export declare function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[];
|
|
19
32
|
export declare function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv;
|
|
20
33
|
export declare function buildDevCommand(runDevEnvironment?: (options: DevOptions) => Promise<void>, verifyProject?: (commandName: string, projectRoot?: string) => void): Command;
|
|
21
34
|
export declare const devCommand: Command;
|
|
35
|
+
export {};
|
|
22
36
|
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAa9C,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAMD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAYD,wBAAgB,uBAAuB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGnE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAezE;AAED,wBAAgB,8BAA8B,CAC5C,eAAe,EAAE,eAAe,EAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,yBAAyB,CAqB3B;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAGnE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,eAAe,GAAG,MAAM,CAEvF;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,SAAS,SAAS,EAClB,UAAU,SAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAUA;AAED,wBAAgB,oCAAoC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAKnF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAW3G;AAsTD,wBAAgB,eAAe,CAC7B,iBAAiB,GAAE,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAuB,EAC/E,aAAa,GAAE,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAA8B,GAC3F,OAAO,CAyBT;AAED,eAAO,MAAM,UAAU,SAAoB,CAAC"}
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -35,12 +35,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.devCommand = void 0;
|
|
37
37
|
exports.buildFunctionsStartArgs = buildFunctionsStartArgs;
|
|
38
|
+
exports.parseCoreToolsVersion = parseCoreToolsVersion;
|
|
39
|
+
exports.compareVersionNumbers = compareVersionNumbers;
|
|
40
|
+
exports.buildFunctionsCoreToolsCommand = buildFunctionsCoreToolsCommand;
|
|
38
41
|
exports.buildNextDevArgs = buildNextDevArgs;
|
|
42
|
+
exports.buildFunctionsBaseUrl = buildFunctionsBaseUrl;
|
|
43
|
+
exports.getFunctionsReadinessTimeoutMs = getFunctionsReadinessTimeoutMs;
|
|
44
|
+
exports.waitForHttpServerReady = waitForHttpServerReady;
|
|
39
45
|
exports.getPythonVirtualEnvPaths = getPythonVirtualEnvPaths;
|
|
46
|
+
exports.getCSharpFunctionsBuildArtifactPaths = getCSharpFunctionsBuildArtifactPaths;
|
|
40
47
|
exports.buildPythonFunctionsEnv = buildPythonFunctionsEnv;
|
|
41
48
|
exports.buildDevCommand = buildDevCommand;
|
|
42
49
|
const commander_1 = require("commander");
|
|
43
50
|
const child_process_1 = require("child_process");
|
|
51
|
+
const http = __importStar(require("http"));
|
|
52
|
+
const https = __importStar(require("https"));
|
|
44
53
|
const path = __importStar(require("path"));
|
|
45
54
|
const fs = __importStar(require("fs"));
|
|
46
55
|
const os = __importStar(require("os"));
|
|
@@ -49,7 +58,10 @@ const cosmos_1 = require("@azure/cosmos");
|
|
|
49
58
|
const config_1 = require("../../core/config");
|
|
50
59
|
const dev_seeds_1 = require("./dev-seeds");
|
|
51
60
|
const package_manager_1 = require("../../utils/package-manager");
|
|
61
|
+
const python_uv_1 = require("../../utils/python-uv");
|
|
52
62
|
const connector_mock_server_1 = require("../../core/mock/connector-mock-server");
|
|
63
|
+
const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
|
|
64
|
+
const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
|
|
53
65
|
function normalizeParsedDevOptions(options) {
|
|
54
66
|
return {
|
|
55
67
|
...options,
|
|
@@ -59,10 +71,64 @@ function normalizeParsedDevOptions(options) {
|
|
|
59
71
|
function buildFunctionsStartArgs(functionsPort) {
|
|
60
72
|
return ['start', '--port', functionsPort];
|
|
61
73
|
}
|
|
74
|
+
function parseCoreToolsVersion(output) {
|
|
75
|
+
const match = output.match(/\d+\.\d+\.\d+/);
|
|
76
|
+
return match ? match[0] : null;
|
|
77
|
+
}
|
|
78
|
+
function compareVersionNumbers(left, right) {
|
|
79
|
+
const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
|
|
80
|
+
const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
|
|
81
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
82
|
+
for (let index = 0; index < length; index += 1) {
|
|
83
|
+
const leftPart = leftParts[index] ?? 0;
|
|
84
|
+
const rightPart = rightParts[index] ?? 0;
|
|
85
|
+
if (leftPart !== rightPart) {
|
|
86
|
+
return leftPart - rightPart;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
function buildFunctionsCoreToolsCommand(backendLanguage, installedVersion) {
|
|
92
|
+
if (backendLanguage === 'csharp' &&
|
|
93
|
+
(!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)) {
|
|
94
|
+
const reason = installedVersion
|
|
95
|
+
? `installed func ${installedVersion} is too old for C# isolated`
|
|
96
|
+
: 'func is not installed';
|
|
97
|
+
return {
|
|
98
|
+
command: 'npm',
|
|
99
|
+
argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
|
|
100
|
+
label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
command: 'func',
|
|
105
|
+
argsPrefix: [],
|
|
106
|
+
label: installedVersion ? `func ${installedVersion}` : 'func',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
62
109
|
function buildNextDevArgs(pm, port) {
|
|
63
110
|
const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
|
|
64
111
|
return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
|
|
65
112
|
}
|
|
113
|
+
function buildFunctionsBaseUrl(host, functionsPort) {
|
|
114
|
+
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
115
|
+
}
|
|
116
|
+
function getFunctionsReadinessTimeoutMs(backendLanguage) {
|
|
117
|
+
return backendLanguage === 'csharp' ? 90000 : 30000;
|
|
118
|
+
}
|
|
119
|
+
async function waitForHttpServerReady(url, timeoutMs = 30000, intervalMs = 500) {
|
|
120
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
|
+
while (Date.now() <= deadline) {
|
|
122
|
+
if (await probeHttpServer(url)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (Date.now() + intervalMs > deadline) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
129
|
+
}
|
|
130
|
+
return probeHttpServer(url);
|
|
131
|
+
}
|
|
66
132
|
function getPythonVirtualEnvPaths(functionsDir) {
|
|
67
133
|
const venvDir = path.join(functionsDir, '.venv');
|
|
68
134
|
const binDir = process.platform === 'win32'
|
|
@@ -73,6 +139,12 @@ function getPythonVirtualEnvPaths(functionsDir) {
|
|
|
73
139
|
: path.join(binDir, 'python');
|
|
74
140
|
return { venvDir, binDir, pythonExecutable };
|
|
75
141
|
}
|
|
142
|
+
function getCSharpFunctionsBuildArtifactPaths(functionsDir) {
|
|
143
|
+
return [
|
|
144
|
+
path.join(functionsDir, 'bin'),
|
|
145
|
+
path.join(functionsDir, 'obj'),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
76
148
|
function buildPythonFunctionsEnv(baseEnv, functionsDir) {
|
|
77
149
|
const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
78
150
|
const pathKey = getPathEnvKey(baseEnv);
|
|
@@ -101,10 +173,12 @@ function prependToPathEnv(env, entry) {
|
|
|
101
173
|
async function checkCoreTools() {
|
|
102
174
|
return checkCommand('func', ['--version']);
|
|
103
175
|
}
|
|
104
|
-
async function checkCommand(command, args = ['--version']) {
|
|
176
|
+
async function checkCommand(command, args = ['--version'], options) {
|
|
105
177
|
return new Promise((resolve) => {
|
|
106
178
|
const checkProcess = (0, child_process_1.spawn)(command, args, {
|
|
107
|
-
|
|
179
|
+
cwd: options?.cwd,
|
|
180
|
+
env: options?.env ?? process.env,
|
|
181
|
+
shell: options?.shell ?? true,
|
|
108
182
|
stdio: 'pipe',
|
|
109
183
|
});
|
|
110
184
|
checkProcess.on('close', (code) => {
|
|
@@ -115,22 +189,22 @@ async function checkCommand(command, args = ['--version']) {
|
|
|
115
189
|
});
|
|
116
190
|
});
|
|
117
191
|
}
|
|
118
|
-
async function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
]
|
|
124
|
-
: [
|
|
125
|
-
{ command: 'python3', argsPrefix: [], label: 'python3' },
|
|
126
|
-
{ command: 'python', argsPrefix: [], label: 'python' },
|
|
127
|
-
];
|
|
128
|
-
for (const candidate of candidates) {
|
|
129
|
-
if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
|
|
130
|
-
return candidate;
|
|
131
|
-
}
|
|
192
|
+
async function resolveProjectLocalUvCommand(projectRoot) {
|
|
193
|
+
const uvEnv = (0, python_uv_1.buildProjectLocalUvEnv)(process.env, projectRoot);
|
|
194
|
+
const { localUvExecutable } = (0, python_uv_1.getProjectLocalUvPaths)(projectRoot);
|
|
195
|
+
if (await checkCommand('uv', ['--version'], { shell: false })) {
|
|
196
|
+
return { command: 'uv', env: uvEnv };
|
|
132
197
|
}
|
|
133
|
-
|
|
198
|
+
if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
|
|
199
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
200
|
+
}
|
|
201
|
+
console.log('📦 Installing project-local uv...');
|
|
202
|
+
const installer = (0, python_uv_1.getProjectLocalUvInstallerCommand)();
|
|
203
|
+
await runCommand(installer.command, installer.args, projectRoot, 'uv installation', (0, python_uv_1.buildProjectLocalUvInstallerEnv)(process.env, projectRoot), false);
|
|
204
|
+
if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
|
|
205
|
+
throw new Error('Failed to install project-local uv.');
|
|
206
|
+
}
|
|
207
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
134
208
|
}
|
|
135
209
|
async function getCommandPath(command) {
|
|
136
210
|
const locator = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -141,6 +215,17 @@ async function getCommandPath(command) {
|
|
|
141
215
|
.find(Boolean);
|
|
142
216
|
return firstLine || null;
|
|
143
217
|
}
|
|
218
|
+
async function resolveInstalledCoreToolsVersion() {
|
|
219
|
+
if (!(await checkCoreTools())) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
144
229
|
async function captureCommandOutput(command, args, cwd, env) {
|
|
145
230
|
return new Promise((resolve, reject) => {
|
|
146
231
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
@@ -168,6 +253,35 @@ async function captureCommandOutput(command, args, cwd, env) {
|
|
|
168
253
|
child.on('error', reject);
|
|
169
254
|
});
|
|
170
255
|
}
|
|
256
|
+
async function probeHttpServer(url) {
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
const target = new URL(url);
|
|
259
|
+
const requestFactory = target.protocol === 'https:' ? https.request : http.request;
|
|
260
|
+
let settled = false;
|
|
261
|
+
const finish = (value) => {
|
|
262
|
+
if (!settled) {
|
|
263
|
+
settled = true;
|
|
264
|
+
resolve(value);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const request = requestFactory({
|
|
268
|
+
hostname: target.hostname,
|
|
269
|
+
port: target.port,
|
|
270
|
+
path: target.pathname || '/',
|
|
271
|
+
method: 'GET',
|
|
272
|
+
timeout: 1000,
|
|
273
|
+
}, (response) => {
|
|
274
|
+
response.resume();
|
|
275
|
+
finish(true);
|
|
276
|
+
});
|
|
277
|
+
request.on('timeout', () => {
|
|
278
|
+
request.destroy();
|
|
279
|
+
finish(false);
|
|
280
|
+
});
|
|
281
|
+
request.on('error', () => finish(false));
|
|
282
|
+
request.end();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
171
285
|
async function resolvePythonRuntimeDetails(functionsDir, env) {
|
|
172
286
|
const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
173
287
|
const output = await captureCommandOutput(pythonExecutable, [
|
|
@@ -216,28 +330,25 @@ async function bridgePythonCoreToolsForWindowsArm64(functionsDir, env) {
|
|
|
216
330
|
return prependToPathEnv(env, patchedRoot);
|
|
217
331
|
}
|
|
218
332
|
async function preparePythonFunctionsEnvironment(functionsDir) {
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
333
|
+
const projectRoot = (0, python_uv_1.getPythonProjectRoot)(functionsDir);
|
|
334
|
+
const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
|
|
335
|
+
const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
336
|
+
const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
|
|
337
|
+
cwd: functionsDir,
|
|
338
|
+
env: uvEnv,
|
|
339
|
+
shell: false,
|
|
340
|
+
});
|
|
341
|
+
if (!hasUsableVirtualEnv) {
|
|
342
|
+
const venvArgs = (0, python_uv_1.buildUvVenvArgs)('.venv');
|
|
343
|
+
if (fs.existsSync(venvDir)) {
|
|
344
|
+
venvArgs.push('--clear');
|
|
230
345
|
}
|
|
346
|
+
console.log('📦 Creating Python virtual environment with uv...');
|
|
347
|
+
await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
|
|
231
348
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await runCommand('uv', ['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'], functionsDir, 'python dependency installation', pythonEnv);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
|
|
239
|
-
await runCommand('python', ['-m', 'pip', 'install', '-r', 'requirements.txt'], functionsDir, 'python dependency installation', pythonEnv);
|
|
240
|
-
}
|
|
349
|
+
console.log('📦 Installing Python Azure Functions dependencies with uv...');
|
|
350
|
+
await runCommand(uvCommand, (0, python_uv_1.buildUvPipInstallArgs)(pythonExecutable, 'requirements.txt'), functionsDir, 'python dependency installation', uvEnv, false);
|
|
351
|
+
const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
|
|
241
352
|
return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
|
|
242
353
|
}
|
|
243
354
|
/**
|
|
@@ -271,7 +382,7 @@ function buildDevCommand(runDevEnvironment = startDevEnvironment, verifyProject
|
|
|
271
382
|
.option('--host <host>', 'Host name', 'localhost')
|
|
272
383
|
.option('--open', 'Open browser automatically', false)
|
|
273
384
|
.option('--verbose', 'Show verbose logs', false)
|
|
274
|
-
.option('--no-functions', 'Skip Azure Functions startup'
|
|
385
|
+
.option('--no-functions', 'Skip Azure Functions startup')
|
|
275
386
|
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
276
387
|
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
277
388
|
.action(async (options) => {
|
|
@@ -387,6 +498,9 @@ async function startDevEnvironment(options) {
|
|
|
387
498
|
let mockServer = null;
|
|
388
499
|
let envLocalPath = '';
|
|
389
500
|
let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
|
|
501
|
+
const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
|
|
502
|
+
let functionsReadinessPromise = null;
|
|
503
|
+
let functionsReady = !!options.noFunctions;
|
|
390
504
|
// Cleanup processes on Ctrl+C
|
|
391
505
|
process.on('SIGINT', async () => {
|
|
392
506
|
console.log('\n🛑 Stopping development servers...');
|
|
@@ -431,10 +545,12 @@ async function startDevEnvironment(options) {
|
|
|
431
545
|
// 2. Check if Azure Functions exists
|
|
432
546
|
const functionsDir = path.join(process.cwd(), 'functions');
|
|
433
547
|
const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
|
|
548
|
+
let functionsCoreToolsCommand = null;
|
|
549
|
+
let installedCoreToolsVersion = null;
|
|
434
550
|
if (hasFunctions && !options.noFunctions) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (!
|
|
551
|
+
installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
|
|
552
|
+
functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
553
|
+
if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
|
|
438
554
|
console.log('');
|
|
439
555
|
console.log('⚠️ Azure Functions Core Tools not found.');
|
|
440
556
|
console.log('');
|
|
@@ -481,6 +597,9 @@ async function startDevEnvironment(options) {
|
|
|
481
597
|
options.noFunctions = true;
|
|
482
598
|
}
|
|
483
599
|
}
|
|
600
|
+
else if (functionsCoreToolsCommand.command !== 'func') {
|
|
601
|
+
console.log(`ℹ️ Using ${functionsCoreToolsCommand.label}.`);
|
|
602
|
+
}
|
|
484
603
|
if (!options.noFunctions) {
|
|
485
604
|
// Check if Cosmos DB Emulator is running
|
|
486
605
|
const cosmosRunning = await checkCosmosDBEmulator();
|
|
@@ -521,8 +640,7 @@ async function startDevEnvironment(options) {
|
|
|
521
640
|
}
|
|
522
641
|
}
|
|
523
642
|
else if (backendLanguage === 'csharp') {
|
|
524
|
-
console.log('
|
|
525
|
-
await runCommand('dotnet', ['build'], functionsDir, 'dotnet build');
|
|
643
|
+
console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
|
|
526
644
|
}
|
|
527
645
|
else {
|
|
528
646
|
functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
|
|
@@ -574,7 +692,8 @@ async function startDevEnvironment(options) {
|
|
|
574
692
|
}
|
|
575
693
|
}
|
|
576
694
|
// Azure Functions を起動
|
|
577
|
-
const
|
|
695
|
+
const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
696
|
+
const funcProcess = (0, child_process_1.spawn)(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
|
|
578
697
|
cwd: functionsDir,
|
|
579
698
|
shell: true,
|
|
580
699
|
stdio: 'pipe', // Always pipe to capture output
|
|
@@ -625,7 +744,8 @@ async function startDevEnvironment(options) {
|
|
|
625
744
|
}
|
|
626
745
|
}
|
|
627
746
|
});
|
|
628
|
-
console.log(
|
|
747
|
+
console.log(`⏳ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
|
|
748
|
+
functionsReadinessPromise = waitForHttpServerReady(functionsBaseUrl, getFunctionsReadinessTimeoutMs(backendLanguage));
|
|
629
749
|
}
|
|
630
750
|
else if (!hasFunctions) {
|
|
631
751
|
console.log('');
|
|
@@ -714,8 +834,8 @@ async function startDevEnvironment(options) {
|
|
|
714
834
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
715
835
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
716
836
|
envLocalPath = path.join(process.cwd(), '.env.local');
|
|
717
|
-
envLocalDefaultUrl =
|
|
718
|
-
const bffTargetUrl =
|
|
837
|
+
envLocalDefaultUrl = functionsBaseUrl;
|
|
838
|
+
const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
|
|
719
839
|
try {
|
|
720
840
|
if (fs.existsSync(envLocalPath)) {
|
|
721
841
|
const envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
@@ -729,8 +849,8 @@ async function startDevEnvironment(options) {
|
|
|
729
849
|
catch { /* ignore */ }
|
|
730
850
|
const nextEnv = {
|
|
731
851
|
...process.env,
|
|
732
|
-
BACKEND_FUNCTIONS_BASE_URL:
|
|
733
|
-
FUNCTIONS_BASE_URL:
|
|
852
|
+
BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
853
|
+
FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
734
854
|
};
|
|
735
855
|
const nextProcess = (0, child_process_1.spawn)(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
|
|
736
856
|
cwd: process.cwd(),
|
|
@@ -758,20 +878,33 @@ async function startDevEnvironment(options) {
|
|
|
758
878
|
});
|
|
759
879
|
process.exit(code || 0);
|
|
760
880
|
});
|
|
881
|
+
if (functionsReadinessPromise) {
|
|
882
|
+
functionsReady = await functionsReadinessPromise;
|
|
883
|
+
console.log('');
|
|
884
|
+
if (functionsReady) {
|
|
885
|
+
console.log(`✅ Azure Functions ready (port: ${functionsPort})`);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
console.log(`⚠️ Azure Functions is still starting: ${functionsBaseUrl}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
761
891
|
console.log('');
|
|
762
892
|
console.log('✅ SwallowKit development environment is running!');
|
|
763
893
|
console.log('');
|
|
764
894
|
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
765
895
|
if (hasFunctions && !options.noFunctions) {
|
|
766
|
-
console.log(
|
|
896
|
+
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
767
897
|
}
|
|
768
898
|
if (mockServer) {
|
|
769
|
-
console.log(`🔌 Mock Proxy:
|
|
899
|
+
console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
|
|
770
900
|
}
|
|
771
901
|
console.log('');
|
|
772
|
-
if (hasFunctions && !options.noFunctions) {
|
|
902
|
+
if (hasFunctions && !options.noFunctions && functionsReady) {
|
|
773
903
|
console.log('💡 Azure Functions and Next.js BFF are connected');
|
|
774
904
|
}
|
|
905
|
+
else if (hasFunctions && !options.noFunctions) {
|
|
906
|
+
console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
|
|
907
|
+
}
|
|
775
908
|
if (mockServer) {
|
|
776
909
|
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
777
910
|
}
|
|
@@ -789,11 +922,11 @@ async function startDevEnvironment(options) {
|
|
|
789
922
|
process.exit(1);
|
|
790
923
|
}
|
|
791
924
|
}
|
|
792
|
-
async function runCommand(command, args, cwd, label, env) {
|
|
925
|
+
async function runCommand(command, args, cwd, label, env, shell = true) {
|
|
793
926
|
await new Promise((resolve, reject) => {
|
|
794
927
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
795
928
|
cwd,
|
|
796
|
-
shell
|
|
929
|
+
shell,
|
|
797
930
|
stdio: 'inherit',
|
|
798
931
|
env,
|
|
799
932
|
});
|