swallowkit 1.0.0-beta.5 → 1.0.0-beta.51
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/LICENSE +21 -21
- package/README.ja.md +153 -242
- package/README.md +153 -243
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +13 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +759 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +19 -81
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +57 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +470 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +37 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +720 -156
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +73 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3089 -1779
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +13 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +513 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +5 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +206 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +14 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +172 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +63 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +226 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +63 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1714 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1029 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +922 -220
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +333 -139
- 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 +696 -0
- package/dist/core/scaffold/native-schema-generator.js.map +1 -0
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +326 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +13 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +772 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +156 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +202 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +5 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +34 -13
- package/dist/utils/package-manager.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/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +49 -0
- package/dist/version.js.map +1 -0
- package/package.json +84 -74
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +761 -0
- package/src/__tests__/config.test.ts +295 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +173 -0
- package/src/__tests__/dev.test.ts +287 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +356 -0
- package/src/__tests__/jest-globals.d.ts +8 -0
- package/src/__tests__/machine.test.ts +356 -0
- package/src/__tests__/mcp.test.ts +145 -0
- package/src/__tests__/model-parser.test.ts +52 -0
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/__tests__/nextjs-generator.test.ts +105 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +227 -0
- package/src/__tests__/python-uv.test.ts +48 -0
- package/src/__tests__/scaffold.test.ts +191 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/tsconfig.json +15 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +815 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +614 -0
- package/src/cli/commands/dev.ts +1234 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3902 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1067 -0
- package/src/cli/index.ts +202 -0
- package/src/core/config.ts +379 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +245 -0
- package/src/core/scaffold/auth-generator.ts +1760 -0
- package/src/core/scaffold/connector-functions-generator.ts +1133 -0
- package/src/core/scaffold/functions-generator.ts +972 -0
- package/src/core/scaffold/model-parser.ts +847 -0
- package/src/core/scaffold/native-schema-generator.ts +822 -0
- package/src/core/scaffold/nextjs-generator.ts +382 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1068 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +209 -0
- package/src/types/index.ts +165 -0
- package/src/utils/package-manager.ts +246 -0
- package/src/utils/python-uv.ts +96 -0
- package/src/version.ts +13 -0
- package/dist/cli/commands/build.d.ts +0 -6
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/build.js +0 -177
- package/dist/cli/commands/build.js.map +0 -1
- package/dist/cli/commands/deploy.d.ts +0 -3
- package/dist/cli/commands/deploy.d.ts.map +0 -1
- package/dist/cli/commands/deploy.js +0 -147
- package/dist/cli/commands/deploy.js.map +0 -1
- package/dist/cli/commands/setup.d.ts +0 -6
- package/dist/cli/commands/setup.d.ts.map +0 -1
- package/dist/cli/commands/setup.js +0 -254
- package/dist/cli/commands/setup.js.map +0 -1
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import * as https from 'https';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as net from 'net';
|
|
9
|
+
import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
|
|
10
|
+
import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
|
|
11
|
+
import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
12
|
+
import {
|
|
13
|
+
applyDevSeedEnvironment,
|
|
14
|
+
getContainerNameForModel,
|
|
15
|
+
getDefaultCosmosDatabaseName,
|
|
16
|
+
loadProjectModels,
|
|
17
|
+
resolveLocalCosmosConnectionInfo,
|
|
18
|
+
} from './dev-seeds';
|
|
19
|
+
import { BackendLanguage } from '../../types';
|
|
20
|
+
import { detectFromProject, getCommands } from '../../utils/package-manager';
|
|
21
|
+
import {
|
|
22
|
+
buildProjectLocalUvEnv,
|
|
23
|
+
buildProjectLocalUvInstallerEnv,
|
|
24
|
+
buildUvPipInstallArgs,
|
|
25
|
+
buildUvVenvArgs,
|
|
26
|
+
getProjectLocalUvInstallerCommand,
|
|
27
|
+
getProjectLocalUvPaths,
|
|
28
|
+
getPythonProjectRoot,
|
|
29
|
+
} from '../../utils/python-uv';
|
|
30
|
+
import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
|
|
31
|
+
|
|
32
|
+
export interface DevOptions {
|
|
33
|
+
port?: string;
|
|
34
|
+
functionsPort?: string;
|
|
35
|
+
host?: string;
|
|
36
|
+
open?: boolean;
|
|
37
|
+
verbose?: boolean;
|
|
38
|
+
noFunctions?: boolean;
|
|
39
|
+
seedEnv?: string;
|
|
40
|
+
mockConnectors?: boolean;
|
|
41
|
+
swaPort?: string;
|
|
42
|
+
noSwa?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ParsedDevActionOptions = DevOptions & {
|
|
46
|
+
functions?: boolean;
|
|
47
|
+
swa?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
interface FunctionsCoreToolsCommand {
|
|
51
|
+
command: string;
|
|
52
|
+
argsPrefix: string[];
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
|
|
57
|
+
const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
|
|
58
|
+
|
|
59
|
+
function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions {
|
|
60
|
+
return {
|
|
61
|
+
...options,
|
|
62
|
+
noFunctions: options.noFunctions ?? options.functions === false,
|
|
63
|
+
noSwa: options.noSwa ?? options.swa === false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildFunctionsStartArgs(functionsPort: string): string[] {
|
|
68
|
+
return ['start', '--port', functionsPort];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseCoreToolsVersion(output: string): string | null {
|
|
72
|
+
const match = output.match(/\d+\.\d+\.\d+/);
|
|
73
|
+
return match ? match[0] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function compareVersionNumbers(left: string, right: string): number {
|
|
77
|
+
const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
|
|
78
|
+
const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
|
|
79
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < length; index += 1) {
|
|
82
|
+
const leftPart = leftParts[index] ?? 0;
|
|
83
|
+
const rightPart = rightParts[index] ?? 0;
|
|
84
|
+
|
|
85
|
+
if (leftPart !== rightPart) {
|
|
86
|
+
return leftPart - rightPart;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildFunctionsCoreToolsCommand(
|
|
94
|
+
backendLanguage: BackendLanguage,
|
|
95
|
+
installedVersion: string | null
|
|
96
|
+
): FunctionsCoreToolsCommand {
|
|
97
|
+
if (
|
|
98
|
+
backendLanguage === 'csharp' &&
|
|
99
|
+
(!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)
|
|
100
|
+
) {
|
|
101
|
+
const reason = installedVersion
|
|
102
|
+
? `installed func ${installedVersion} is too old for C# isolated`
|
|
103
|
+
: 'func is not installed';
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
command: 'npm',
|
|
107
|
+
argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
|
|
108
|
+
label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
command: 'func',
|
|
114
|
+
argsPrefix: [],
|
|
115
|
+
label: installedVersion ? `func ${installedVersion}` : 'func',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildNextDevArgs(pm: string, port: string): string[] {
|
|
120
|
+
const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
|
|
121
|
+
return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string {
|
|
125
|
+
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
|
|
129
|
+
return [
|
|
130
|
+
'start',
|
|
131
|
+
`http://${host || 'localhost'}:${nextPort}`,
|
|
132
|
+
'--swa-config-location',
|
|
133
|
+
'.',
|
|
134
|
+
'--port',
|
|
135
|
+
swaPort,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getSwaCliInstallCommand(pm: string): string {
|
|
140
|
+
return pm === 'pnpm'
|
|
141
|
+
? 'pnpm add -Dw @azure/static-web-apps-cli'
|
|
142
|
+
: 'npm install -D @azure/static-web-apps-cli';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
|
|
146
|
+
return backendLanguage === 'csharp' ? 90_000 : 30_000;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function waitForHttpServerReady(
|
|
150
|
+
url: string,
|
|
151
|
+
timeoutMs = 30_000,
|
|
152
|
+
intervalMs = 500
|
|
153
|
+
): Promise<boolean> {
|
|
154
|
+
const deadline = Date.now() + timeoutMs;
|
|
155
|
+
|
|
156
|
+
while (Date.now() <= deadline) {
|
|
157
|
+
if (await probeHttpServer(url)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Date.now() + intervalMs > deadline) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return probeHttpServer(url);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function getPythonVirtualEnvPaths(functionsDir: string): {
|
|
172
|
+
venvDir: string;
|
|
173
|
+
binDir: string;
|
|
174
|
+
pythonExecutable: string;
|
|
175
|
+
} {
|
|
176
|
+
const venvDir = path.join(functionsDir, '.venv');
|
|
177
|
+
const binDir = process.platform === 'win32'
|
|
178
|
+
? path.join(venvDir, 'Scripts')
|
|
179
|
+
: path.join(venvDir, 'bin');
|
|
180
|
+
const pythonExecutable = process.platform === 'win32'
|
|
181
|
+
? path.join(binDir, 'python.exe')
|
|
182
|
+
: path.join(binDir, 'python');
|
|
183
|
+
|
|
184
|
+
return { venvDir, binDir, pythonExecutable };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[] {
|
|
188
|
+
return [
|
|
189
|
+
path.join(functionsDir, 'bin'),
|
|
190
|
+
path.join(functionsDir, 'obj'),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv {
|
|
195
|
+
const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
196
|
+
const pathKey = getPathEnvKey(baseEnv);
|
|
197
|
+
const currentPath = baseEnv[pathKey] || '';
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...baseEnv,
|
|
201
|
+
[pathKey]: currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir,
|
|
202
|
+
VIRTUAL_ENV: venvDir,
|
|
203
|
+
languageWorkers__python__defaultExecutablePath: pythonExecutable,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getPathEnvKey(env: NodeJS.ProcessEnv): string {
|
|
208
|
+
return Object.keys(env).find((key) => key.toUpperCase() === 'PATH') || 'PATH';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function prependToPathEnv(env: NodeJS.ProcessEnv, entry: string): NodeJS.ProcessEnv {
|
|
212
|
+
const pathKey = getPathEnvKey(env);
|
|
213
|
+
const currentPath = env[pathKey] || '';
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...env,
|
|
217
|
+
[pathKey]: currentPath ? `${entry}${path.delimiter}${currentPath}` : entry,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if Azure Functions Core Tools is installed
|
|
223
|
+
*/
|
|
224
|
+
async function checkCoreTools(): Promise<boolean> {
|
|
225
|
+
return checkCommand('func', ['--version']);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function checkCommand(
|
|
229
|
+
command: string,
|
|
230
|
+
args: string[] = ['--version'],
|
|
231
|
+
options?: {
|
|
232
|
+
cwd?: string;
|
|
233
|
+
env?: NodeJS.ProcessEnv;
|
|
234
|
+
shell?: boolean;
|
|
235
|
+
}
|
|
236
|
+
): Promise<boolean> {
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
const checkProcess = spawn(command, args, {
|
|
239
|
+
cwd: options?.cwd,
|
|
240
|
+
env: options?.env ?? process.env,
|
|
241
|
+
shell: options?.shell ?? true,
|
|
242
|
+
stdio: 'pipe',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
checkProcess.on('close', (code) => {
|
|
246
|
+
resolve(code === 0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
checkProcess.on('error', () => {
|
|
250
|
+
resolve(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
256
|
+
const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
|
|
257
|
+
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
258
|
+
|
|
259
|
+
if (await checkCommand('uv', ['--version'], { shell: false })) {
|
|
260
|
+
return { command: 'uv', env: uvEnv };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
|
|
264
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log('📦 Installing project-local uv...');
|
|
268
|
+
const installer = getProjectLocalUvInstallerCommand();
|
|
269
|
+
await runCommand(
|
|
270
|
+
installer.command,
|
|
271
|
+
installer.args,
|
|
272
|
+
projectRoot,
|
|
273
|
+
'uv installation',
|
|
274
|
+
buildProjectLocalUvInstallerEnv(process.env, projectRoot),
|
|
275
|
+
false
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
|
|
279
|
+
throw new Error('Failed to install project-local uv.');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { command: localUvExecutable, env: uvEnv };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function getCommandPath(command: string): Promise<string | null> {
|
|
286
|
+
const locator = process.platform === 'win32' ? 'where' : 'which';
|
|
287
|
+
const result = await captureCommandOutput(locator, [command]);
|
|
288
|
+
const firstLine = result
|
|
289
|
+
.split(/\r?\n/)
|
|
290
|
+
.map((line) => line.trim())
|
|
291
|
+
.find(Boolean);
|
|
292
|
+
|
|
293
|
+
return firstLine || null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function resolveInstalledCoreToolsVersion(): Promise<string | null> {
|
|
297
|
+
if (!(await checkCoreTools())) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function captureCommandOutput(
|
|
309
|
+
command: string,
|
|
310
|
+
args: string[],
|
|
311
|
+
cwd?: string,
|
|
312
|
+
env?: NodeJS.ProcessEnv
|
|
313
|
+
): Promise<string> {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const child = spawn(command, args, {
|
|
316
|
+
cwd,
|
|
317
|
+
env,
|
|
318
|
+
shell: false,
|
|
319
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
let stdout = '';
|
|
323
|
+
let stderr = '';
|
|
324
|
+
|
|
325
|
+
child.stdout?.on('data', (data) => {
|
|
326
|
+
stdout += data.toString();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
child.stderr?.on('data', (data) => {
|
|
330
|
+
stderr += data.toString();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
child.on('close', (code) => {
|
|
334
|
+
if (code === 0) {
|
|
335
|
+
resolve(stdout);
|
|
336
|
+
} else {
|
|
337
|
+
reject(new Error(stderr || stdout || `${command} exited with code ${code}`));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
child.on('error', reject);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function probeHttpServer(url: string): Promise<boolean> {
|
|
346
|
+
return new Promise((resolve) => {
|
|
347
|
+
const target = new URL(url);
|
|
348
|
+
const requestFactory = target.protocol === 'https:' ? https.request : http.request;
|
|
349
|
+
let settled = false;
|
|
350
|
+
const finish = (value: boolean) => {
|
|
351
|
+
if (!settled) {
|
|
352
|
+
settled = true;
|
|
353
|
+
resolve(value);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const request = requestFactory(
|
|
358
|
+
{
|
|
359
|
+
hostname: target.hostname,
|
|
360
|
+
port: target.port,
|
|
361
|
+
path: target.pathname || '/',
|
|
362
|
+
method: 'GET',
|
|
363
|
+
timeout: 1000,
|
|
364
|
+
},
|
|
365
|
+
(response) => {
|
|
366
|
+
response.resume();
|
|
367
|
+
finish(true);
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
request.on('timeout', () => {
|
|
372
|
+
request.destroy();
|
|
373
|
+
finish(false);
|
|
374
|
+
});
|
|
375
|
+
request.on('error', () => finish(false));
|
|
376
|
+
request.end();
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function resolvePythonRuntimeDetails(
|
|
381
|
+
functionsDir: string,
|
|
382
|
+
env: NodeJS.ProcessEnv
|
|
383
|
+
): Promise<{ version: string; architecture: string }> {
|
|
384
|
+
const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
385
|
+
const output = await captureCommandOutput(
|
|
386
|
+
pythonExecutable,
|
|
387
|
+
[
|
|
388
|
+
'-c',
|
|
389
|
+
'import platform; import sys; print(str(sys.version_info.major) + "." + str(sys.version_info.minor)); print(platform.machine())',
|
|
390
|
+
],
|
|
391
|
+
functionsDir,
|
|
392
|
+
env
|
|
393
|
+
);
|
|
394
|
+
const [version, architecture] = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
395
|
+
|
|
396
|
+
if (!version || !architecture) {
|
|
397
|
+
throw new Error('Failed to determine Python runtime details.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { version, architecture };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function bridgePythonCoreToolsForWindowsArm64(
|
|
404
|
+
functionsDir: string,
|
|
405
|
+
env: NodeJS.ProcessEnv
|
|
406
|
+
): Promise<NodeJS.ProcessEnv> {
|
|
407
|
+
if (process.platform !== 'win32' || process.arch !== 'arm64') {
|
|
408
|
+
return env;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const funcPath = await getCommandPath('func');
|
|
412
|
+
if (!funcPath) {
|
|
413
|
+
return env;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const { version, architecture } = await resolvePythonRuntimeDetails(functionsDir, env);
|
|
417
|
+
if (architecture.toUpperCase() !== 'AMD64') {
|
|
418
|
+
return env;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const coreToolsRoot = path.dirname(funcPath);
|
|
422
|
+
const armWorkerDir = path.join(coreToolsRoot, 'workers', 'python', version, 'WINDOWS', 'Arm64');
|
|
423
|
+
if (fs.existsSync(armWorkerDir)) {
|
|
424
|
+
return env;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const x64WorkerDir = path.join(coreToolsRoot, 'workers', 'python', version, 'WINDOWS', 'X64');
|
|
428
|
+
if (!fs.existsSync(x64WorkerDir)) {
|
|
429
|
+
return env;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const patchedRoot = path.join(
|
|
433
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
|
|
434
|
+
'SwallowKit',
|
|
435
|
+
'azure-functions-core-tools-python-bridge'
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (!fs.existsSync(path.join(patchedRoot, 'func.exe'))) {
|
|
439
|
+
console.log('🩹 Creating a local Azure Functions Core Tools bridge for Windows Arm64 Python...');
|
|
440
|
+
fs.mkdirSync(path.dirname(patchedRoot), { recursive: true });
|
|
441
|
+
fs.cpSync(coreToolsRoot, patchedRoot, { recursive: true });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const patchedArmWorkerDir = path.join(patchedRoot, 'workers', 'python', version, 'WINDOWS', 'Arm64');
|
|
445
|
+
const patchedX64WorkerDir = path.join(patchedRoot, 'workers', 'python', version, 'WINDOWS', 'X64');
|
|
446
|
+
if (!fs.existsSync(patchedArmWorkerDir) && fs.existsSync(patchedX64WorkerDir)) {
|
|
447
|
+
fs.cpSync(patchedX64WorkerDir, patchedArmWorkerDir, { recursive: true });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
console.log(`🩹 Using bridged Azure Functions Core Tools from ${patchedRoot}`);
|
|
451
|
+
return prependToPathEnv(env, patchedRoot);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
|
|
455
|
+
const projectRoot = getPythonProjectRoot(functionsDir);
|
|
456
|
+
const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
|
|
457
|
+
const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
|
|
458
|
+
const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
|
|
459
|
+
cwd: functionsDir,
|
|
460
|
+
env: uvEnv,
|
|
461
|
+
shell: false,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (!hasUsableVirtualEnv) {
|
|
465
|
+
const venvArgs = buildUvVenvArgs('.venv');
|
|
466
|
+
if (fs.existsSync(venvDir)) {
|
|
467
|
+
venvArgs.push('--clear');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log('📦 Creating Python virtual environment with uv...');
|
|
471
|
+
await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
console.log('📦 Installing Python Azure Functions dependencies with uv...');
|
|
475
|
+
await runCommand(
|
|
476
|
+
uvCommand,
|
|
477
|
+
buildUvPipInstallArgs(pythonExecutable, 'requirements.txt'),
|
|
478
|
+
functionsDir,
|
|
479
|
+
'python dependency installation',
|
|
480
|
+
uvEnv,
|
|
481
|
+
false
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
|
|
485
|
+
return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Check if Cosmos DB Emulator is running by checking if port 8081 is open
|
|
490
|
+
*/
|
|
491
|
+
async function checkCosmosDBEmulator(): Promise<boolean> {
|
|
492
|
+
return new Promise((resolve) => {
|
|
493
|
+
const socket = new net.Socket();
|
|
494
|
+
|
|
495
|
+
const timeout = setTimeout(() => {
|
|
496
|
+
socket.destroy();
|
|
497
|
+
resolve(false);
|
|
498
|
+
}, 2000);
|
|
499
|
+
|
|
500
|
+
socket.on('connect', () => {
|
|
501
|
+
clearTimeout(timeout);
|
|
502
|
+
socket.destroy();
|
|
503
|
+
resolve(true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
socket.on('error', () => {
|
|
507
|
+
clearTimeout(timeout);
|
|
508
|
+
resolve(false);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
socket.connect(8081, 'localhost');
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function buildDevCommand(
|
|
516
|
+
runDevEnvironment: (options: DevOptions) => Promise<void> = startDevEnvironment,
|
|
517
|
+
verifyProject: (commandName: string, projectRoot?: string) => void = ensureSwallowKitProject
|
|
518
|
+
): Command {
|
|
519
|
+
return new Command()
|
|
520
|
+
.name('dev')
|
|
521
|
+
.description('Start SwallowKit development server (Cosmos DB + Next.js + Azure Functions)')
|
|
522
|
+
.option('-p, --port <port>', 'Next.js port', '3000')
|
|
523
|
+
.option('-f, --functions-port <port>', 'Azure Functions port', '7071')
|
|
524
|
+
.option('--host <host>', 'Host name', 'localhost')
|
|
525
|
+
.option('--open', 'Open browser automatically', false)
|
|
526
|
+
.option('--verbose', 'Show verbose logs', false)
|
|
527
|
+
.option('--no-functions', 'Skip Azure Functions startup')
|
|
528
|
+
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
529
|
+
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
530
|
+
.option('--swa-port <port>', 'SWA authentication emulator port', '4280')
|
|
531
|
+
.option('--no-swa', 'Skip the SWA authentication emulator')
|
|
532
|
+
.action(async (options: ParsedDevActionOptions) => {
|
|
533
|
+
const normalizedOptions = normalizeParsedDevOptions(options);
|
|
534
|
+
|
|
535
|
+
// SwallowKit プロジェクトディレクトリかどうかを検証
|
|
536
|
+
verifyProject("dev");
|
|
537
|
+
|
|
538
|
+
console.log('🚀 Starting SwallowKit development environment...');
|
|
539
|
+
if (normalizedOptions.verbose) {
|
|
540
|
+
console.log('⚙️ Options:', normalizedOptions);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
await runDevEnvironment(normalizedOptions);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export const devCommand = buildDevCommand();
|
|
548
|
+
|
|
549
|
+
interface CosmosInitializationResult {
|
|
550
|
+
endpoint: string;
|
|
551
|
+
key: string;
|
|
552
|
+
databaseName: string;
|
|
553
|
+
models: ModelInfo[];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializationResult | null> {
|
|
557
|
+
try {
|
|
558
|
+
const connectionInfoResult = resolveLocalCosmosConnectionInfo(databaseName);
|
|
559
|
+
if (!connectionInfoResult.ok) {
|
|
560
|
+
if (connectionInfoResult.reason === 'missing-local-settings') {
|
|
561
|
+
console.log('⚠️ local.settings.json not found. Skipping Cosmos DB initialization.');
|
|
562
|
+
} else if (connectionInfoResult.reason === 'missing-connection-string') {
|
|
563
|
+
console.log('⚠️ CosmosDBConnection not found in local.settings.json. Skipping Cosmos DB initialization.');
|
|
564
|
+
} else {
|
|
565
|
+
console.log('⚠️ Invalid CosmosDB connection string format.');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
console.log('🗄️ Initializing Cosmos DB...');
|
|
572
|
+
const { endpoint, key, databaseName: dbName } = connectionInfoResult.value;
|
|
573
|
+
|
|
574
|
+
const client = new CosmosClient({
|
|
575
|
+
endpoint: endpoint,
|
|
576
|
+
key,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Create database if not exists
|
|
580
|
+
const { database } = await client.databases.createIfNotExists({ id: dbName });
|
|
581
|
+
console.log(`✅ Database "${dbName}" ready`);
|
|
582
|
+
|
|
583
|
+
const models = await loadProjectModels();
|
|
584
|
+
for (const model of models) {
|
|
585
|
+
const containerName = getContainerNameForModel(model);
|
|
586
|
+
|
|
587
|
+
// Try creating container with full partition key definition first
|
|
588
|
+
let containerCreated = false;
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
console.log(`🔧 Creating container "${containerName}" with partition key ${model.partitionKey}...`);
|
|
592
|
+
const containerResponse = await database.containers.createIfNotExists({
|
|
593
|
+
id: containerName,
|
|
594
|
+
partitionKey: {
|
|
595
|
+
paths: [model.partitionKey],
|
|
596
|
+
kind: PartitionKeyKind.Hash,
|
|
597
|
+
version: 2
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
|
|
601
|
+
containerCreated = true;
|
|
602
|
+
} catch (error: unknown) {
|
|
603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
604
|
+
console.log(`⚠️ Failed with full partition key definition: ${message}`);
|
|
605
|
+
console.log(`🔄 Retrying with simple partition key...`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// If first attempt failed, try with simple partition key definition
|
|
609
|
+
if (!containerCreated) {
|
|
610
|
+
try {
|
|
611
|
+
const containerResponse = await database.containers.createIfNotExists({
|
|
612
|
+
id: containerName,
|
|
613
|
+
partitionKey: {
|
|
614
|
+
paths: [model.partitionKey]
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
console.log(`✅ Container "${containerName}" ready (status: ${containerResponse.statusCode})`);
|
|
618
|
+
} catch (containerError: any) {
|
|
619
|
+
console.error(`❌ Failed to create container "${containerName}":`, containerError.message);
|
|
620
|
+
console.error(`Error code: ${containerError.code}`);
|
|
621
|
+
if (containerError.body) {
|
|
622
|
+
console.error(`Response body:`, JSON.stringify(containerError.body, null, 2));
|
|
623
|
+
}
|
|
624
|
+
// Continue with other containers
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log('✅ Cosmos DB initialization complete\n');
|
|
630
|
+
return { endpoint, key, databaseName: dbName, models };
|
|
631
|
+
} catch (error: any) {
|
|
632
|
+
console.error('⚠️ Cosmos DB initialization failed:', error.message);
|
|
633
|
+
if (error.stack) {
|
|
634
|
+
console.error('Stack trace:', error.stack);
|
|
635
|
+
}
|
|
636
|
+
console.log('💡 Make sure Cosmos DB Emulator is running');
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function startDevEnvironment(options: DevOptions) {
|
|
642
|
+
const port = options.port || '3000';
|
|
643
|
+
const functionsPort = options.functionsPort || '7071';
|
|
644
|
+
const swaPort = options.swaPort || '4280';
|
|
645
|
+
|
|
646
|
+
// Detect package manager from project lockfile
|
|
647
|
+
const pm = detectFromProject();
|
|
648
|
+
const pmCmd = getCommands(pm);
|
|
649
|
+
const backendLanguage = getBackendLanguage();
|
|
650
|
+
const authConfig = getAuthConfig();
|
|
651
|
+
const normalizedAuth = normalizeAuthConfig(authConfig);
|
|
652
|
+
const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
|
|
653
|
+
const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
|
|
654
|
+
|
|
655
|
+
if (useSwaEmulator && !swaCommand) {
|
|
656
|
+
const installCommand = getSwaCliInstallCommand(pm);
|
|
657
|
+
console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
|
|
658
|
+
console.error(' Install it in this project and retry:');
|
|
659
|
+
console.error(`\n ${installCommand}\n`);
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// プロセスを管理する配列
|
|
665
|
+
const processes: ChildProcess[] = [];
|
|
666
|
+
let functionsEnv: NodeJS.ProcessEnv = process.env;
|
|
667
|
+
let mockServer: ConnectorMockServer | null = null;
|
|
668
|
+
let envLocalPath = '';
|
|
669
|
+
let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
|
|
670
|
+
const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
|
|
671
|
+
let functionsReadinessPromise: Promise<boolean> | null = null;
|
|
672
|
+
let functionsReady = !!options.noFunctions;
|
|
673
|
+
|
|
674
|
+
// Cleanup processes on Ctrl+C
|
|
675
|
+
process.on('SIGINT', async () => {
|
|
676
|
+
console.log('\n🛑 Stopping development servers...');
|
|
677
|
+
// Restore .env.local to default Functions port on shutdown
|
|
678
|
+
if (envLocalPath && envLocalDefaultUrl) {
|
|
679
|
+
try {
|
|
680
|
+
if (fs.existsSync(envLocalPath)) {
|
|
681
|
+
const content = fs.readFileSync(envLocalPath, 'utf-8');
|
|
682
|
+
if (content.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
|
|
683
|
+
!content.includes(`BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`)) {
|
|
684
|
+
const restored = content.replace(
|
|
685
|
+
/^BACKEND_FUNCTIONS_BASE_URL=.*/m,
|
|
686
|
+
`BACKEND_FUNCTIONS_BASE_URL=${envLocalDefaultUrl}`
|
|
687
|
+
);
|
|
688
|
+
fs.writeFileSync(envLocalPath, restored, 'utf-8');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
} catch { /* ignore */ }
|
|
692
|
+
}
|
|
693
|
+
if (mockServer) {
|
|
694
|
+
await mockServer.stop();
|
|
695
|
+
}
|
|
696
|
+
processes.forEach((proc) => {
|
|
697
|
+
if (proc && !proc.killed) {
|
|
698
|
+
proc.kill();
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
process.exit(0);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
// 1. Check for Next.js project
|
|
706
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
707
|
+
const nextConfigPathJs = path.join(process.cwd(), 'next.config.js');
|
|
708
|
+
const nextConfigPathTs = path.join(process.cwd(), 'next.config.ts');
|
|
709
|
+
const nextConfigPathMjs = path.join(process.cwd(), 'next.config.mjs');
|
|
710
|
+
|
|
711
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
712
|
+
console.log('❌ package.json not found.');
|
|
713
|
+
console.log('💡 Please run this command in the root directory of a Next.js project.');
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!fs.existsSync(nextConfigPathJs) && !fs.existsSync(nextConfigPathTs) && !fs.existsSync(nextConfigPathMjs)) {
|
|
718
|
+
console.log('⚠️ next.config file not found. Is this a Next.js project?');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 2. Check if Azure Functions exists
|
|
722
|
+
const functionsDir = path.join(process.cwd(), 'functions');
|
|
723
|
+
const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
|
|
724
|
+
|
|
725
|
+
let functionsCoreToolsCommand: FunctionsCoreToolsCommand | null = null;
|
|
726
|
+
let installedCoreToolsVersion: string | null = null;
|
|
727
|
+
|
|
728
|
+
if (hasFunctions && !options.noFunctions) {
|
|
729
|
+
installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
|
|
730
|
+
functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
731
|
+
|
|
732
|
+
if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
|
|
733
|
+
console.log('');
|
|
734
|
+
console.log('⚠️ Azure Functions Core Tools not found.');
|
|
735
|
+
console.log('');
|
|
736
|
+
|
|
737
|
+
// Prompt user for installation
|
|
738
|
+
const readline = await import('readline');
|
|
739
|
+
const rl = readline.createInterface({
|
|
740
|
+
input: process.stdin,
|
|
741
|
+
output: process.stdout
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const answer = await new Promise<string>((resolve) => {
|
|
745
|
+
rl.question('Would you like to install Azure Functions Core Tools? (y/n): ', resolve);
|
|
746
|
+
});
|
|
747
|
+
rl.close();
|
|
748
|
+
|
|
749
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
750
|
+
console.log('📦 Installing Azure Functions Core Tools...');
|
|
751
|
+
console.log(' This may take a few minutes.');
|
|
752
|
+
|
|
753
|
+
const installProcess = spawn(pm, pm === 'pnpm' ? ['add', '-g', 'azure-functions-core-tools@4'] : ['install', '-g', 'azure-functions-core-tools@4'], {
|
|
754
|
+
shell: true,
|
|
755
|
+
stdio: 'inherit',
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
await new Promise<void>((resolve, reject) => {
|
|
759
|
+
installProcess.on('close', (code) => {
|
|
760
|
+
if (code === 0) {
|
|
761
|
+
console.log('✅ Azure Functions Core Tools installed successfully.');
|
|
762
|
+
resolve();
|
|
763
|
+
} else {
|
|
764
|
+
console.error('❌ Installation failed.');
|
|
765
|
+
console.log('💡 Please install manually:');
|
|
766
|
+
console.log(` ${pmCmd.addGlobal} azure-functions-core-tools@4`);
|
|
767
|
+
reject(new Error(`Installation failed with code ${code}`));
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
installProcess.on('error', reject);
|
|
771
|
+
});
|
|
772
|
+
} else {
|
|
773
|
+
console.log('');
|
|
774
|
+
console.log('ℹ️ Skipping Azure Functions startup.');
|
|
775
|
+
console.log('💡 To install later:');
|
|
776
|
+
console.log(` ${pmCmd.addGlobal} azure-functions-core-tools@4`);
|
|
777
|
+
console.log('');
|
|
778
|
+
// Skip Azure Functions startup
|
|
779
|
+
options.noFunctions = true;
|
|
780
|
+
}
|
|
781
|
+
} else if (functionsCoreToolsCommand.command !== 'func') {
|
|
782
|
+
console.log(`ℹ️ Using ${functionsCoreToolsCommand.label}.`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!options.noFunctions) {
|
|
786
|
+
// Check if Cosmos DB Emulator is running
|
|
787
|
+
const cosmosRunning = await checkCosmosDBEmulator();
|
|
788
|
+
|
|
789
|
+
if (!cosmosRunning) {
|
|
790
|
+
console.log('');
|
|
791
|
+
console.log('❌ Cosmos DB Emulator is not running.');
|
|
792
|
+
console.log('');
|
|
793
|
+
console.log('💡 Please start Cosmos DB Emulator manually:');
|
|
794
|
+
console.log(' C:\\Program Files\\Azure Cosmos DB Emulator\\CosmosDB.Emulator.exe');
|
|
795
|
+
console.log('');
|
|
796
|
+
console.log(' Or search for "Azure Cosmos DB Emulator" in the Start menu.');
|
|
797
|
+
console.log('');
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Initialize Cosmos DB before starting Functions
|
|
802
|
+
const databaseName = getDefaultCosmosDatabaseName();
|
|
803
|
+
const cosmosInitialization = await initializeCosmosDB(databaseName);
|
|
804
|
+
if (options.seedEnv && cosmosInitialization) {
|
|
805
|
+
await applyDevSeedEnvironment({
|
|
806
|
+
client: new CosmosClient({
|
|
807
|
+
endpoint: cosmosInitialization.endpoint,
|
|
808
|
+
key: cosmosInitialization.key,
|
|
809
|
+
}),
|
|
810
|
+
databaseName: cosmosInitialization.databaseName,
|
|
811
|
+
environment: options.seedEnv,
|
|
812
|
+
models: cosmosInitialization.models,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
console.log('');
|
|
817
|
+
console.log('🚀 Starting Azure Functions...');
|
|
818
|
+
|
|
819
|
+
if (backendLanguage === 'typescript') {
|
|
820
|
+
const functionsNodeModules = path.join(functionsDir, 'node_modules');
|
|
821
|
+
if (!fs.existsSync(functionsNodeModules)) {
|
|
822
|
+
console.log('📦 Installing Azure Functions dependencies...');
|
|
823
|
+
await runCommand(pm, ['install'], functionsDir, `${pm} install`);
|
|
824
|
+
}
|
|
825
|
+
} else if (backendLanguage === 'csharp') {
|
|
826
|
+
console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
|
|
827
|
+
} else {
|
|
828
|
+
functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (hasFunctions && !options.noFunctions) {
|
|
834
|
+
// Build shared package before starting Functions
|
|
835
|
+
const sharedDir = path.join(process.cwd(), 'shared');
|
|
836
|
+
const sharedPkgPath = path.join(sharedDir, 'package.json');
|
|
837
|
+
if (fs.existsSync(sharedDir) && fs.existsSync(sharedPkgPath)) {
|
|
838
|
+
const sharedPkg = JSON.parse(fs.readFileSync(sharedPkgPath, 'utf-8'));
|
|
839
|
+
if (sharedPkg.scripts?.build) {
|
|
840
|
+
console.log('📦 Building shared package...');
|
|
841
|
+
const filterArgs = pm === 'pnpm'
|
|
842
|
+
? ['run', '--filter', 'shared', 'build']
|
|
843
|
+
: ['run', '--workspace=shared', 'build'];
|
|
844
|
+
const sharedBuild = spawn(pm, filterArgs, {
|
|
845
|
+
cwd: process.cwd(),
|
|
846
|
+
shell: true,
|
|
847
|
+
stdio: 'inherit',
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
await new Promise<void>((resolve, reject) => {
|
|
851
|
+
sharedBuild.on('close', (code) => {
|
|
852
|
+
if (code === 0) {
|
|
853
|
+
console.log('✅ Shared package built successfully');
|
|
854
|
+
resolve();
|
|
855
|
+
} else {
|
|
856
|
+
reject(new Error(`Shared package build failed with code ${code}`));
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
sharedBuild.on('error', reject);
|
|
860
|
+
});
|
|
861
|
+
} else {
|
|
862
|
+
console.log('⚠️ Shared package has no build script — skipping build. Run "swallowkit add-auth" to fix.');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Build TypeScript functions after shared package (functions import from shared)
|
|
867
|
+
if (backendLanguage === 'typescript') {
|
|
868
|
+
const functionsPkgPath = path.join(functionsDir, 'package.json');
|
|
869
|
+
if (fs.existsSync(functionsPkgPath)) {
|
|
870
|
+
const functionsPkg = JSON.parse(fs.readFileSync(functionsPkgPath, 'utf-8'));
|
|
871
|
+
if (functionsPkg.scripts?.build) {
|
|
872
|
+
console.log('📦 Building TypeScript Azure Functions...');
|
|
873
|
+
await runCommand(pm, ['run', 'build'], functionsDir, `${pm} run build`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Azure Functions を起動
|
|
879
|
+
const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
880
|
+
const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
|
|
881
|
+
cwd: functionsDir,
|
|
882
|
+
shell: true,
|
|
883
|
+
stdio: 'pipe', // Always pipe to capture output
|
|
884
|
+
env: functionsEnv
|
|
885
|
+
});
|
|
886
|
+
let pythonWorkerMissing = false;
|
|
887
|
+
|
|
888
|
+
// Functions の出力をそのまま表示(プレフィックス付き)
|
|
889
|
+
if (funcProcess.stdout) {
|
|
890
|
+
funcProcess.stdout.on('data', (data) => {
|
|
891
|
+
const output = data.toString();
|
|
892
|
+
// 各行にプレフィックスを付けて出力
|
|
893
|
+
const lines = output.split('\n').filter((line: string) => line.trim());
|
|
894
|
+
lines.forEach((line: string) => {
|
|
895
|
+
if (
|
|
896
|
+
backendLanguage === 'python' &&
|
|
897
|
+
(line.includes('WorkerConfig for runtime: python not found') || line.includes('DefaultWorkerPath:'))
|
|
898
|
+
) {
|
|
899
|
+
pythonWorkerMissing = true;
|
|
900
|
+
}
|
|
901
|
+
console.log(`[Functions] ${line}`);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (funcProcess.stderr) {
|
|
907
|
+
funcProcess.stderr.on('data', (data) => {
|
|
908
|
+
const output = data.toString();
|
|
909
|
+
const lines = output.split('\n').filter((line: string) => line.trim());
|
|
910
|
+
lines.forEach((line: string) => {
|
|
911
|
+
if (
|
|
912
|
+
backendLanguage === 'python' &&
|
|
913
|
+
(line.includes('WorkerConfig for runtime: python not found') || line.includes('DefaultWorkerPath:'))
|
|
914
|
+
) {
|
|
915
|
+
pythonWorkerMissing = true;
|
|
916
|
+
}
|
|
917
|
+
console.error(`[Functions Error] ${line}`);
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
processes.push(funcProcess);
|
|
923
|
+
|
|
924
|
+
funcProcess.on('error', (error) => {
|
|
925
|
+
console.error('⚠️ Azure Functions startup error:', error.message);
|
|
926
|
+
console.log('💡 Please ensure Azure Functions Core Tools is installed');
|
|
927
|
+
console.log(` ${pmCmd.addGlobal} azure-functions-core-tools@4`);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
funcProcess.on('close', (code) => {
|
|
931
|
+
if (code !== 0) {
|
|
932
|
+
console.log(`\n⏹️ Azure Functions exited (exit code: ${code})`);
|
|
933
|
+
if (backendLanguage === 'python' && pythonWorkerMissing) {
|
|
934
|
+
console.log('💡 Your Azure Functions Core Tools installation is missing the Python worker for this OS/architecture.');
|
|
935
|
+
console.log(' Reinstall a matching Core Tools v4 package (Windows users should prefer the official x64/x86 MSI for their machine).');
|
|
936
|
+
console.log(' SwallowKit local Python dev uses functions/.venv and requirements.txt, but Core Tools still needs its own bundled Python worker.');
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
console.log(`⏳ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
|
|
942
|
+
functionsReadinessPromise = waitForHttpServerReady(
|
|
943
|
+
functionsBaseUrl,
|
|
944
|
+
getFunctionsReadinessTimeoutMs(backendLanguage)
|
|
945
|
+
);
|
|
946
|
+
} else if (!hasFunctions) {
|
|
947
|
+
console.log('');
|
|
948
|
+
console.log('ℹ️ functions/ directory not found. Starting Next.js only.');
|
|
949
|
+
} else if (options.noFunctions) {
|
|
950
|
+
console.log('');
|
|
951
|
+
console.log('ℹ️ --no-functions specified. Skipping Azure Functions.');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
console.log('');
|
|
955
|
+
console.log('🚀 Starting Next.js development server...');
|
|
956
|
+
|
|
957
|
+
// Mock connector server — start proxy if --mock-connectors is enabled
|
|
958
|
+
let bffTargetPort = functionsPort;
|
|
959
|
+
|
|
960
|
+
if (options.mockConnectors) {
|
|
961
|
+
const allModels = await loadProjectModels();
|
|
962
|
+
const connectorModels = allModels.filter((m) => m.connectorConfig);
|
|
963
|
+
|
|
964
|
+
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
965
|
+
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
966
|
+
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
967
|
+
// Read JWT_SECRET from functions/local.settings.json if available
|
|
968
|
+
let jwtSecret = 'dev-jwt-secret-change-in-production-min-32-chars!!';
|
|
969
|
+
try {
|
|
970
|
+
const localSettingsPath = path.join(process.cwd(), 'functions', 'local.settings.json');
|
|
971
|
+
if (fs.existsSync(localSettingsPath)) {
|
|
972
|
+
const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf-8'));
|
|
973
|
+
jwtSecret = localSettings.Values?.[authConfig.customJwt.jwtSecretEnv || 'JWT_SECRET'] || jwtSecret;
|
|
974
|
+
}
|
|
975
|
+
} catch { /* ignore */ }
|
|
976
|
+
mockAuthConfig = {
|
|
977
|
+
jwtSecret,
|
|
978
|
+
tokenExpiry: authConfig.customJwt.tokenExpiry,
|
|
979
|
+
customJwt: {
|
|
980
|
+
userTable: authConfig.customJwt.userTable,
|
|
981
|
+
loginIdColumn: authConfig.customJwt.loginIdColumn,
|
|
982
|
+
passwordHashColumn: authConfig.customJwt.passwordHashColumn,
|
|
983
|
+
rolesColumn: authConfig.customJwt.rolesColumn,
|
|
984
|
+
},
|
|
985
|
+
defaultPolicy: authConfig.authorization?.defaultPolicy === "public" ? "anonymous" :
|
|
986
|
+
authConfig.authorization?.defaultPolicy === "authenticated" || authConfig.authorization?.defaultPolicy === "anonymous"
|
|
987
|
+
? authConfig.authorization.defaultPolicy : undefined,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (connectorModels.length > 0 || mockAuthConfig) {
|
|
992
|
+
const mockPort = parseInt(functionsPort, 10) + 1;
|
|
993
|
+
mockServer = new ConnectorMockServer({
|
|
994
|
+
port: mockPort,
|
|
995
|
+
functionsTarget: `${options.host || 'localhost'}:${functionsPort}`,
|
|
996
|
+
connectorModels,
|
|
997
|
+
allModels,
|
|
998
|
+
seedEnv: options.seedEnv,
|
|
999
|
+
host: options.host || 'localhost',
|
|
1000
|
+
authConfig: mockAuthConfig,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
await mockServer.start();
|
|
1004
|
+
bffTargetPort = String(mockPort);
|
|
1005
|
+
|
|
1006
|
+
const modelCount = connectorModels.length + (mockAuthConfig ? 1 : 0);
|
|
1007
|
+
console.log('');
|
|
1008
|
+
console.log(`🔌 Mock server started (port: ${mockPort}) — ${modelCount} model(s) mocked via Zod/seed data`);
|
|
1009
|
+
for (const m of connectorModels) {
|
|
1010
|
+
const ops = m.connectorConfig!.operations.join(', ');
|
|
1011
|
+
console.log(` - ${m.name} [${ops}]`);
|
|
1012
|
+
}
|
|
1013
|
+
if (mockAuthConfig) {
|
|
1014
|
+
console.log(` - auth [login, me, logout]`);
|
|
1015
|
+
}
|
|
1016
|
+
console.log(` Other routes → proxied to Azure Functions (port: ${functionsPort})`);
|
|
1017
|
+
} else {
|
|
1018
|
+
console.log('');
|
|
1019
|
+
console.log('ℹ️ --mock-connectors specified but no connector models found. Skipping mock server.');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 5. Start Next.js development server
|
|
1024
|
+
const nextArgs = buildNextDevArgs(pm, port);
|
|
1025
|
+
|
|
1026
|
+
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1027
|
+
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
1028
|
+
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
1029
|
+
envLocalPath = path.join(process.cwd(), '.env.local');
|
|
1030
|
+
envLocalDefaultUrl = functionsBaseUrl;
|
|
1031
|
+
const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
|
|
1032
|
+
try {
|
|
1033
|
+
if (fs.existsSync(envLocalPath)) {
|
|
1034
|
+
const envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
1035
|
+
if (envContent.includes('BACKEND_FUNCTIONS_BASE_URL=') &&
|
|
1036
|
+
!envContent.includes(`BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`)) {
|
|
1037
|
+
const updated = envContent.replace(
|
|
1038
|
+
/^BACKEND_FUNCTIONS_BASE_URL=.*/m,
|
|
1039
|
+
`BACKEND_FUNCTIONS_BASE_URL=${bffTargetUrl}`
|
|
1040
|
+
);
|
|
1041
|
+
fs.writeFileSync(envLocalPath, updated, 'utf-8');
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
} catch { /* ignore */ }
|
|
1045
|
+
|
|
1046
|
+
const nextEnv: NodeJS.ProcessEnv = {
|
|
1047
|
+
...process.env,
|
|
1048
|
+
BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
1049
|
+
FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
|
|
1053
|
+
cwd: process.cwd(),
|
|
1054
|
+
shell: true,
|
|
1055
|
+
stdio: options.verbose ? 'inherit' : 'inherit',
|
|
1056
|
+
env: nextEnv,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
processes.push(nextProcess);
|
|
1060
|
+
|
|
1061
|
+
nextProcess.on('error', (error) => {
|
|
1062
|
+
console.error('❌ Next.js startup error:', error.message);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
nextProcess.on('close', (code) => {
|
|
1067
|
+
if (code !== 0) {
|
|
1068
|
+
console.log(`\n⏹️ Next.js exited (exit code: ${code})`);
|
|
1069
|
+
}
|
|
1070
|
+
// Exit all processes when Next.js exits
|
|
1071
|
+
if (mockServer) {
|
|
1072
|
+
mockServer.stop().catch(() => {});
|
|
1073
|
+
}
|
|
1074
|
+
processes.forEach((proc) => {
|
|
1075
|
+
if (proc && !proc.killed) {
|
|
1076
|
+
proc.kill();
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
process.exit(code || 0);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (functionsReadinessPromise) {
|
|
1083
|
+
functionsReady = await functionsReadinessPromise;
|
|
1084
|
+
console.log('');
|
|
1085
|
+
if (functionsReady) {
|
|
1086
|
+
console.log(`✅ Azure Functions ready (port: ${functionsPort})`);
|
|
1087
|
+
} else {
|
|
1088
|
+
console.log(`⚠️ Azure Functions is still starting: ${functionsBaseUrl}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
let swaReady = false;
|
|
1093
|
+
if (useSwaEmulator && swaCommand) {
|
|
1094
|
+
const nextUrl = `http://${options.host || 'localhost'}:${port}`;
|
|
1095
|
+
console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
|
|
1096
|
+
const nextReady = await waitForHttpServerReady(nextUrl);
|
|
1097
|
+
if (!nextReady) {
|
|
1098
|
+
throw new Error(`Next.js did not become ready at ${nextUrl}`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
|
|
1102
|
+
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
|
|
1103
|
+
cwd: process.cwd(),
|
|
1104
|
+
shell: true,
|
|
1105
|
+
stdio: 'inherit',
|
|
1106
|
+
env: process.env,
|
|
1107
|
+
});
|
|
1108
|
+
processes.push(swaProcess);
|
|
1109
|
+
swaProcess.on('error', (error) => {
|
|
1110
|
+
console.error('❌ SWA CLI startup error:', error.message);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1114
|
+
swaReady = await waitForHttpServerReady(swaUrl);
|
|
1115
|
+
if (!swaReady) {
|
|
1116
|
+
throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
console.log('');
|
|
1121
|
+
console.log('✅ SwallowKit development environment is running!');
|
|
1122
|
+
console.log('');
|
|
1123
|
+
if (useSwaEmulator) {
|
|
1124
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1125
|
+
console.log('🔐 SWA authenticated app:');
|
|
1126
|
+
console.log(` ${swaUrl}`);
|
|
1127
|
+
console.log('');
|
|
1128
|
+
console.log('🔑 Local sign-in:');
|
|
1129
|
+
console.log(` ${swaUrl}/.auth/login/aad`);
|
|
1130
|
+
console.log(' Enter any username and select Login.');
|
|
1131
|
+
console.log(' The "authenticated" role is added automatically.');
|
|
1132
|
+
console.log('');
|
|
1133
|
+
console.log('📱 Next.js direct access (authentication is not emulated):');
|
|
1134
|
+
console.log(` http://${options.host || 'localhost'}:${port}`);
|
|
1135
|
+
} else {
|
|
1136
|
+
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (hasFunctions && !options.noFunctions) {
|
|
1139
|
+
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
1140
|
+
}
|
|
1141
|
+
if (mockServer) {
|
|
1142
|
+
console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
|
|
1143
|
+
}
|
|
1144
|
+
console.log('');
|
|
1145
|
+
if (hasFunctions && !options.noFunctions && functionsReady) {
|
|
1146
|
+
console.log('💡 Azure Functions and Next.js BFF are connected');
|
|
1147
|
+
} else if (hasFunctions && !options.noFunctions) {
|
|
1148
|
+
console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
|
|
1149
|
+
}
|
|
1150
|
+
if (mockServer) {
|
|
1151
|
+
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
1152
|
+
}
|
|
1153
|
+
if (swaReady) {
|
|
1154
|
+
console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
|
|
1155
|
+
}
|
|
1156
|
+
console.log('');
|
|
1157
|
+
|
|
1158
|
+
if (options.open) {
|
|
1159
|
+
const url = useSwaEmulator
|
|
1160
|
+
? `http://${options.host || 'localhost'}:${swaPort}`
|
|
1161
|
+
: `http://${options.host || 'localhost'}:${port}`;
|
|
1162
|
+
console.log(`🌐 Opening browser: ${url}`);
|
|
1163
|
+
const start = process.platform === 'darwin' ? 'open' :
|
|
1164
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1165
|
+
spawn(start, [url], { shell: true });
|
|
1166
|
+
console.log('');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
console.log('🛑 Press Ctrl+C to stop');
|
|
1170
|
+
console.log('');
|
|
1171
|
+
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
console.error('❌ Failed to start development environment:', error instanceof Error ? error.message : error);
|
|
1174
|
+
processes.forEach((proc) => {
|
|
1175
|
+
if (proc && !proc.killed) {
|
|
1176
|
+
proc.kill();
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
|
|
1184
|
+
const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
|
|
1185
|
+
const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
|
|
1186
|
+
if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
|
|
1187
|
+
cwd: projectRoot,
|
|
1188
|
+
shell: process.platform === 'win32',
|
|
1189
|
+
})) {
|
|
1190
|
+
return localCommand;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
|
|
1194
|
+
return 'swa';
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function runCommand(
|
|
1201
|
+
command: string,
|
|
1202
|
+
args: string[],
|
|
1203
|
+
cwd: string,
|
|
1204
|
+
label: string,
|
|
1205
|
+
env?: NodeJS.ProcessEnv,
|
|
1206
|
+
shell = true
|
|
1207
|
+
): Promise<void> {
|
|
1208
|
+
await new Promise<void>((resolve, reject) => {
|
|
1209
|
+
const child = spawn(command, args, {
|
|
1210
|
+
cwd,
|
|
1211
|
+
shell,
|
|
1212
|
+
stdio: 'inherit',
|
|
1213
|
+
env,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
child.on('close', (code) => {
|
|
1217
|
+
if (code === 0) {
|
|
1218
|
+
resolve();
|
|
1219
|
+
} else {
|
|
1220
|
+
reject(new Error(`${label} failed with code ${code}`));
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
child.on('error', reject);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function hasFunctionsProject(functionsDir: string, backendLanguage: BackendLanguage): boolean {
|
|
1229
|
+
if (backendLanguage === 'typescript') {
|
|
1230
|
+
return fs.existsSync(path.join(functionsDir, 'package.json'));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return fs.existsSync(path.join(functionsDir, 'host.json'));
|
|
1234
|
+
}
|