swallowkit 1.1.2 → 2.0.0-alpha.1

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.
Files changed (87) hide show
  1. package/README.ja.md +47 -7
  2. package/README.md +47 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +66 -21
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/dev.d.ts +4 -1
  7. package/dist/cli/commands/dev.d.ts.map +1 -1
  8. package/dist/cli/commands/dev.js +108 -44
  9. package/dist/cli/commands/dev.js.map +1 -1
  10. package/dist/cli/commands/init.d.ts +6 -5
  11. package/dist/cli/commands/init.d.ts.map +1 -1
  12. package/dist/cli/commands/init.js +83 -39
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/scaffold.d.ts +1 -1
  15. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  16. package/dist/cli/commands/scaffold.js +38 -15
  17. package/dist/cli/commands/scaffold.js.map +1 -1
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +6 -3
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/core/config.d.ts +2 -1
  22. package/dist/core/config.d.ts.map +1 -1
  23. package/dist/core/config.js +19 -0
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/project/agent-instructions.d.ts +2 -1
  26. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  27. package/dist/core/project/agent-instructions.js +12 -0
  28. package/dist/core/project/agent-instructions.js.map +1 -1
  29. package/dist/core/project/capabilities.d.ts +7 -0
  30. package/dist/core/project/capabilities.d.ts.map +1 -1
  31. package/dist/core/project/capabilities.js +7 -0
  32. package/dist/core/project/capabilities.js.map +1 -1
  33. package/dist/core/project/manifest.d.ts +3 -1
  34. package/dist/core/project/manifest.d.ts.map +1 -1
  35. package/dist/core/project/manifest.js +47 -25
  36. package/dist/core/project/manifest.js.map +1 -1
  37. package/dist/core/project/react-project.d.ts +26 -0
  38. package/dist/core/project/react-project.d.ts.map +1 -0
  39. package/dist/core/project/react-project.js +422 -0
  40. package/dist/core/project/react-project.js.map +1 -0
  41. package/dist/core/project/validation.d.ts.map +1 -1
  42. package/dist/core/project/validation.js +32 -10
  43. package/dist/core/project/validation.js.map +1 -1
  44. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  45. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  46. package/dist/core/scaffold/react-bff-generator.js +252 -0
  47. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  48. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  49. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  50. package/dist/core/scaffold/react-ui-generator.js +41 -0
  51. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  52. package/dist/index.d.ts +3 -3
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/machine/index.js +2 -2
  57. package/dist/machine/index.js.map +1 -1
  58. package/dist/types/index.d.ts +4 -0
  59. package/dist/types/index.d.ts.map +1 -1
  60. package/dist/utils/package-manager.d.ts +2 -2
  61. package/dist/utils/package-manager.d.ts.map +1 -1
  62. package/dist/utils/package-manager.js +4 -1
  63. package/dist/utils/package-manager.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/__tests__/agent-instructions.test.ts +1 -1
  66. package/src/__tests__/init.test.ts +29 -5
  67. package/src/__tests__/react.test.ts +819 -0
  68. package/src/__tests__/release-metadata.test.ts +121 -0
  69. package/src/cli/commands/add-auth.ts +67 -23
  70. package/src/cli/commands/dev.ts +109 -46
  71. package/src/cli/commands/init.ts +75 -35
  72. package/src/cli/commands/scaffold.ts +40 -19
  73. package/src/cli/index.ts +6 -3
  74. package/src/core/config.ts +21 -1
  75. package/src/core/project/agent-instructions.ts +14 -1
  76. package/src/core/project/agent-templates/AGENTS.md +24 -9
  77. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  78. package/src/core/project/capabilities.ts +14 -0
  79. package/src/core/project/manifest.ts +52 -27
  80. package/src/core/project/react-project.ts +507 -0
  81. package/src/core/project/validation.ts +32 -10
  82. package/src/core/scaffold/react-bff-generator.ts +292 -0
  83. package/src/core/scaffold/react-ui-generator.ts +51 -0
  84. package/src/index.ts +3 -1
  85. package/src/machine/index.ts +2 -2
  86. package/src/types/index.ts +4 -0
  87. package/src/utils/package-manager.ts +5 -2
@@ -0,0 +1,121 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { spawnSync } from "child_process";
4
+
5
+ describe("npm release metadata", () => {
6
+ const repository = process.cwd();
7
+ const script = path.join(repository, "scripts", "release-metadata.cjs");
8
+ let directory: string;
9
+
10
+ beforeEach(() => {
11
+ directory = fs.mkdtempSync(path.join(repository, ".tmp-release-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(directory, { recursive: true, force: true });
16
+ });
17
+
18
+ function run(
19
+ version: unknown,
20
+ options: {
21
+ refType?: string;
22
+ refName?: string;
23
+ lockVersion?: string;
24
+ rootVersion?: string;
25
+ noRootPackage?: boolean;
26
+ } = {}
27
+ ) {
28
+ fs.writeFileSync(
29
+ path.join(directory, "package.json"),
30
+ JSON.stringify({ name: "swallowkit", version })
31
+ );
32
+ fs.writeFileSync(
33
+ path.join(directory, "package-lock.json"),
34
+ JSON.stringify({
35
+ version: options.lockVersion ?? version,
36
+ packages: options.noRootPackage
37
+ ? {}
38
+ : { "": { version: options.rootVersion ?? version } },
39
+ })
40
+ );
41
+ const outputPath = path.join(directory, "output.txt");
42
+ const result = spawnSync(process.execPath, [script], {
43
+ cwd: directory,
44
+ encoding: "utf8",
45
+ env: {
46
+ ...process.env,
47
+ GITHUB_REF_TYPE: options.refType ?? "tag",
48
+ GITHUB_REF_NAME: options.refName ?? `v${version}`,
49
+ GITHUB_OUTPUT: outputPath,
50
+ },
51
+ });
52
+ expect(result.error).toBeUndefined();
53
+ const output = fs.existsSync(outputPath)
54
+ ? fs.readFileSync(outputPath, "utf8")
55
+ : "";
56
+ return { ...result, output };
57
+ }
58
+
59
+ it.each([
60
+ ["2.0.0", "latest"],
61
+ ["2.0.0-alpha.1", "alpha"],
62
+ ["2.0.0-alpha.2", "alpha"],
63
+ ["2.0.0-alpha", "alpha"],
64
+ ["2.0.0-beta.1", "beta"],
65
+ ["2.0.0-beta.12", "beta"],
66
+ ["2.0.0-rc.1", "next"],
67
+ ["2.0.0-next.1", "next"],
68
+ ["2.0.0-alphabet.1", "next"],
69
+ ["2.0.0-0", "next"],
70
+ ["2.0.0+build.1", "latest"],
71
+ ["2.0.0-alpha.1+build.1", "alpha"],
72
+ ])("publishes %s on the %s channel", (version, channel) => {
73
+ const result = run(version);
74
+ expect(result.status).toBe(0);
75
+ expect(result.stderr).toBe("");
76
+ expect(result.output).toBe(`version=${version}\ndist_tag=${channel}\n`);
77
+ expect(result.stdout).toBe(result.output);
78
+ });
79
+
80
+ it("supports manual branch dry runs without requiring a release tag", () => {
81
+ const result = run("2.0.0-alpha.1", {
82
+ refType: "branch",
83
+ refName: "main",
84
+ });
85
+ expect(result.status).toBe(0);
86
+ expect(result.output).toContain("dist_tag=alpha\n");
87
+ });
88
+
89
+ it.each([
90
+ { refName: "v2.0.0" },
91
+ { refName: "2.0.0-alpha.1" },
92
+ { lockVersion: "1.1.2" },
93
+ { rootVersion: "1.1.2" },
94
+ { noRootPackage: true },
95
+ ])("rejects inconsistent release metadata: %j", (options) => {
96
+ const result = run("2.0.0-alpha.1", options);
97
+ expect(result.status).not.toBe(0);
98
+ expect(result.stderr).toContain("must match");
99
+ expect(result.output).toBe("");
100
+ });
101
+
102
+ it.each([
103
+ undefined,
104
+ 2,
105
+ "",
106
+ "v2.0.0-alpha.1",
107
+ "2.0",
108
+ "02.0.0",
109
+ "2.0.0-",
110
+ "2.0.0-alpha.01",
111
+ "2.0.0-alpha..1",
112
+ "2.0.0-alpha_1",
113
+ "2.0.0\n",
114
+ "2.0.0-alpha.1\ndist_tag=latest",
115
+ ])("rejects invalid versions before emitting Actions outputs: %j", (version) => {
116
+ const result = run(version);
117
+ expect(result.status).not.toBe(0);
118
+ expect(result.stderr).toContain("must be a valid SemVer version");
119
+ expect(result.output).toBe("");
120
+ });
121
+ });
@@ -5,8 +5,10 @@
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
- import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
9
- import { AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
8
+ import { ensureSwallowKitProject, getBackendLanguage, getFrontendFramework, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
9
+ import { generateManagedFunctionConfig, generateReactAuthBff, generateReactBffHelper } from "../../core/scaffold/react-bff-generator";
10
+ import { adaptReactPage } from "../../core/scaffold/react-ui-generator";
11
+ import { AuthConfig, AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
10
12
  import {
11
13
  generateAuthModels,
12
14
  generateAuthFunctionsTS,
@@ -98,6 +100,32 @@ function defaultCustomJwtConfig(): CustomJwtConfig {
98
100
  };
99
101
  }
100
102
 
103
+ function writeCallFunction(cwd: string, session: FileOperationSession, nextSource: string, auth: AuthConfig): void {
104
+ if (getFrontendFramework() === "react") {
105
+ const name = JSON.parse(session.readFile(path.join(cwd, "package.json"))).name;
106
+ session.writeFile(path.join(cwd, "bff", "src", "lib", "call-function.ts"), generateReactBffHelper(name, auth), managedMeta);
107
+ } else {
108
+ session.writeFile(path.join(cwd, "lib", "api", "call-function.ts"), nextSource, managedMeta);
109
+ }
110
+ }
111
+
112
+ function writeLoginPage(cwd: string, session: FileOperationSession, source: string): void {
113
+ const react = getFrontendFramework() === "react";
114
+ const destination = react ? path.join(cwd, "src", "pages", "login.tsx") : path.join(cwd, "app", "login", "page.tsx");
115
+ session.writeFile(destination, react ? adaptReactPage(source) : source, managedMeta);
116
+ }
117
+
118
+ function writeAuthContext(cwd: string, session: FileOperationSession, source: string): void {
119
+ session.writeFile(path.join(cwd, "lib", "auth", "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(source) : source, managedMeta);
120
+ }
121
+
122
+ function writeManagedAuthRoute(cwd: string, session: FileOperationSession, customJwt: boolean): void {
123
+ const name = JSON.parse(session.readFile(path.join(cwd, "package.json"))).name;
124
+ const sharedName = JSON.parse(session.readFile(path.join(cwd, "shared", "package.json"))).name;
125
+ session.writeFile(path.join(cwd, "bff", "src", "auth", "index.ts"), generateReactAuthBff(name, sharedName, customJwt), managedMeta);
126
+ session.writeFile(path.join(cwd, "bff", "auth", "function.json"), generateManagedFunctionConfig("auth/{action}", ["GET", "POST"], "auth"), managedMeta);
127
+ }
128
+
101
129
  function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage, session: FileOperationSession, allowedProviders?: string[]): void {
102
130
  const providers = normalizeSwaProviders(allowedProviders);
103
131
  const helperPath = backendLanguage === "typescript"
@@ -112,14 +140,12 @@ function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage, session: Fi
112
140
  : generateSwaAuthHelperPython();
113
141
  session.writeFile(helperPath, helper, managedMeta);
114
142
 
115
- const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
116
- session.writeFile(callFnPath, generateBFFCallFunctionWithSwaAuth(), managedMeta);
143
+ writeCallFunction(cwd, session, generateBFFCallFunctionWithSwaAuth(), { provider: "swa" });
117
144
 
118
145
  const authDir = path.join(cwd, "lib", "auth");
119
146
  session.writeFile(path.join(authDir, "auth-context.tsx"), generateSwaAuthContext(providers), managedMeta);
120
147
  updateRootLayoutWithAuthProvider(cwd, session);
121
- const loginDir = path.join(cwd, "app", "login");
122
- session.writeFile(path.join(loginDir, "page.tsx"), generateSwaLoginPage(providers), managedMeta);
148
+ writeLoginPage(cwd, session, generateSwaLoginPage(providers));
123
149
  updateSwaRouteConfig(cwd, session, providers);
124
150
  }
125
151
 
@@ -127,10 +153,10 @@ function setupExternalTokenAuth(cwd: string, backendLanguage: BackendLanguage, s
127
153
  const authDir = path.join(cwd, "lib", "auth");
128
154
  writeIfMissing(session, path.join(authDir, "external-token-adapter.ts"), generateExternalTokenAdapter());
129
155
  session.writeFile(path.join(authDir, "authenticated-fetch.ts"), generateAuthenticatedFetch(), managedMeta);
130
- session.writeFile(path.join(authDir, "auth-context.tsx"), generateExternalTokenAuthContext(), managedMeta);
131
- session.writeFile(path.join(cwd, "lib", "api", "call-function.ts"), generateBFFCallFunctionWithAuth(), managedMeta);
132
- const meDir = path.join(cwd, "app", "api", "auth", "me");
133
- session.writeFile(path.join(meDir, "route.ts"), generateExternalTokenBFFMeRoute(), managedMeta);
156
+ writeAuthContext(cwd, session, generateExternalTokenAuthContext());
157
+ writeCallFunction(cwd, session, generateBFFCallFunctionWithAuth(), { provider: "external-token" });
158
+ if (getFrontendFramework() === "react") writeManagedAuthRoute(cwd, session, false);
159
+ else session.writeFile(path.join(cwd, "app", "api", "auth", "me", "route.ts"), generateExternalTokenBFFMeRoute(), managedMeta);
134
160
  updateRootLayoutWithAuthProvider(cwd, session);
135
161
 
136
162
  if (backendLanguage === "typescript") {
@@ -240,6 +266,14 @@ export function addAuthProviderToRootLayout(layout: string): string {
240
266
  }
241
267
 
242
268
  function updateRootLayoutWithAuthProvider(cwd: string, session: FileOperationSession): void {
269
+ if (getFrontendFramework() === "react") {
270
+ const mainPath = path.join(cwd, "src", "main.tsx");
271
+ const main = session.readFile(mainPath);
272
+ if (main.includes("<AuthProvider>")) return;
273
+ if (!main.includes("<App />")) throw new Error("Could not find <App /> in src/main.tsx; cannot install AuthProvider");
274
+ session.writeFile(mainPath, `import { AuthProvider } from '@/lib/auth/auth-context';\n${main.replace("<App />", "<AuthProvider><App /></AuthProvider>")}`, extensionMeta);
275
+ return;
276
+ }
243
277
  const layoutPath = path.join(cwd, "app", "layout.tsx");
244
278
  if (!session.fileExists(layoutPath)) {
245
279
  throw new Error("app/layout.tsx was not found; cannot install AuthProvider");
@@ -364,27 +398,25 @@ export async function addAuthCommand(options: AddAuthOptions) {
364
398
 
365
399
  // 4. Generate proxy
366
400
  console.log("\n Generating proxy...");
367
- const proxyPath = path.join(cwd, "proxy.ts");
368
- session.writeFile(proxyPath, generateProxy(projectName), managedMeta);
369
- console.log(` Created: proxy.ts`);
401
+ if (getFrontendFramework() === "nextjs") {
402
+ const proxyPath = path.join(cwd, "proxy.ts");
403
+ session.writeFile(proxyPath, generateProxy(projectName), managedMeta);
404
+ console.log(` Created: proxy.ts`);
405
+ }
370
406
 
371
407
  // 5. Generate login page
372
408
  console.log("\n Generating login page...");
373
- const loginDir = path.join(cwd, "app", "login");
374
- session.writeFile(path.join(loginDir, "page.tsx"), generateLoginPage(), managedMeta);
375
- console.log(` Created: app/login/page.tsx`);
409
+ writeLoginPage(cwd, session, generateLoginPage());
376
410
 
377
411
  // 6. Generate auth context
378
412
  console.log("\n Generating auth context...");
379
- const authLibDir = path.join(cwd, "lib", "auth");
380
- session.writeFile(path.join(authLibDir, "auth-context.tsx"), generateAuthContext(), managedMeta);
413
+ writeAuthContext(cwd, session, generateAuthContext());
381
414
  console.log(`✅ Created: lib/auth/auth-context.tsx`);
382
415
 
383
416
  // 7. Update callFunction with auth support
384
417
  console.log("\n Updating callFunction with auth support...");
385
- const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
386
- session.writeFile(callFnPath, generateBFFCallFunctionWithAuth(), managedMeta);
387
- console.log(` Updated: lib/api/call-function.ts`);
418
+ writeCallFunction(cwd, session, generateBFFCallFunctionWithAuth(), { provider: "custom-jwt" });
419
+ console.log(` Updated: ${getFrontendFramework() === "react" ? "bff/src/lib" : "lib/api"}/call-function.ts`);
388
420
 
389
421
  // 8. Update swallowkit.config.js
390
422
  console.log("\n Updating configuration...");
@@ -498,11 +530,19 @@ function addNamedScheme(cwd: string, scheme: string, provider: AuthProvider, lan
498
530
  if (provider === "external-token") {
499
531
  writeIfMissing(session, path.join(clientDir, "token-adapter.ts"), generateExternalTokenAdapter());
500
532
  writeIfMissing(session, path.join(clientDir, "authenticated-fetch.ts"), generateAuthenticatedFetch().replace("'./external-token-adapter'", "'./token-adapter'"));
501
- writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), generateExternalTokenAuthContext().replace(/external-token-adapter/g, "token-adapter"));
533
+ const source = generateExternalTokenAuthContext().replace(/external-token-adapter/g, "token-adapter");
534
+ writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(source) : source);
502
535
  } else if (provider === "swa") {
503
536
  writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), generateSwaAuthContext(allowedProviders));
504
537
  } else if (provider === "custom-jwt") {
505
- writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), generateAuthContext());
538
+ writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(generateAuthContext()) : generateAuthContext());
539
+ }
540
+ if (getFrontendFramework() === "react") {
541
+ const existing = getValidatedFullConfig().auth;
542
+ writeCallFunction(cwd, session, "", {
543
+ ...existing, schemes: { ...existing?.schemes, [scheme]: { provider } },
544
+ });
545
+ return;
506
546
  }
507
547
  const callFunctionPath = path.join(cwd, "lib", "api", "call-function.ts");
508
548
  if (!session.fileExists(callFunctionPath)) writeIfMissing(session, callFunctionPath, generateBFFCallFunctionWithMultipleAuth());
@@ -667,6 +707,10 @@ function generateFunctionsAuth(
667
707
  }
668
708
 
669
709
  function generateBFFAuth(cwd: string, projectName: string, sharedPackageName: string, session: FileOperationSession): void {
710
+ if (getFrontendFramework() === "react") {
711
+ writeManagedAuthRoute(cwd, session, true);
712
+ return;
713
+ }
670
714
  const authApiDir = path.join(cwd, "app", "api", "auth");
671
715
 
672
716
  // Login route
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { spawn, ChildProcess } from 'child_process';
2
+ import { spawn, spawnSync, ChildProcess } from 'child_process';
3
3
  import * as http from 'http';
4
4
  import * as https from 'https';
5
5
  import * as path from 'path';
@@ -7,7 +7,7 @@ import * as fs from 'fs';
7
7
  import * as os from 'os';
8
8
  import * as net from 'net';
9
9
  import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
10
- import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
10
+ import { ensureSwallowKitProject, getBackendLanguage, getFrontendFramework, getAuthConfig, normalizeAuthConfig } from '../../core/config';
11
11
  import { ModelInfo } from '../../core/scaffold/model-parser';
12
12
  import {
13
13
  applyDevSeedEnvironment,
@@ -32,6 +32,7 @@ import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
32
32
  export interface DevOptions {
33
33
  port?: string;
34
34
  functionsPort?: string;
35
+ bffPort?: string;
35
36
  host?: string;
36
37
  open?: boolean;
37
38
  verbose?: boolean;
@@ -125,7 +126,7 @@ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: s
125
126
  return `http://${host || 'localhost'}:${functionsPort}`;
126
127
  }
127
128
 
128
- export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
129
+ export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string, bffPort?: string): string[] {
129
130
  return [
130
131
  'start',
131
132
  `http://${host || 'localhost'}:${nextPort}`,
@@ -133,9 +134,15 @@ export function buildSwaStartArgs(host: string | undefined, nextPort: string, sw
133
134
  '.',
134
135
  '--port',
135
136
  swaPort,
137
+ ...(bffPort ? ['--api-devserver-url', buildFunctionsBaseUrl(host, bffPort)] : []),
136
138
  ];
137
139
  }
138
140
 
141
+ export function buildReactDevArgs(pm: string, port: string, host = 'localhost'): string[] {
142
+ const args = ['vite', '--port', port, '--host', host, '--strictPort'];
143
+ return pm === 'pnpm' ? ['exec', ...args] : args;
144
+ }
145
+
139
146
  export function getSwaCliInstallCommand(pm: string): string {
140
147
  return pm === 'pnpm'
141
148
  ? 'pnpm add -Dw @azure/static-web-apps-cli'
@@ -522,23 +529,48 @@ async function checkCosmosDBEmulator(): Promise<boolean> {
522
529
  });
523
530
  }
524
531
 
532
+ export async function assertDevPortAvailable(port: number, host: string): Promise<void> {
533
+ await new Promise<void>((resolve, reject) => {
534
+ const server = net.createServer();
535
+ server.once('error', error => reject(new Error(`Development port ${port} on ${host} is unavailable: ${error.message}`)));
536
+ server.listen({ port, host, exclusive: true }, () => server.close(error => error ? reject(error) : resolve()));
537
+ });
538
+ }
539
+
540
+ function stopDevProcesses(processes: ChildProcess[]): void {
541
+ const active = processes.filter(child => child.pid && !child.killed);
542
+ if (process.platform === 'win32' && active.length) {
543
+ // Windows shell termination alone leaves Vite, Core Tools, and SWA running.
544
+ const ids = active.map(child => child.pid).join(',');
545
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
546
+ `$all = @(Get-CimInstance Win32_Process); function Stop-OwnedTree([int]$id) { foreach ($child in ($all | Where-Object ParentProcessId -eq $id)) { Stop-OwnedTree $child.ProcessId }; Stop-Process -Id $id -Force -ErrorAction SilentlyContinue }; foreach ($id in @(${ids})) { Stop-OwnedTree $id }`,
547
+ ], { windowsHide: true, encoding: 'utf8' });
548
+ if (result.error || result.status !== 0) {
549
+ console.error('❌ Could not stop all development subprocesses:', result.error?.message || result.stderr);
550
+ }
551
+ } else {
552
+ for (const child of active) child.kill();
553
+ }
554
+ }
555
+
525
556
  export function buildDevCommand(
526
557
  runDevEnvironment: (options: DevOptions) => Promise<void> = startDevEnvironment,
527
558
  verifyProject: (commandName: string, projectRoot?: string) => void = ensureSwallowKitProject
528
559
  ): Command {
529
560
  return new Command()
530
561
  .name('dev')
531
- .description('Start SwallowKit development server (Cosmos DB + Next.js + Azure Functions)')
532
- .option('-p, --port <port>', 'Next.js port', '3000')
562
+ .description('Start SwallowKit frontend, BFF, and standalone Azure Functions')
563
+ .option('-p, --port <port>', 'Frontend dev server port', '3000')
533
564
  .option('-f, --functions-port <port>', 'Azure Functions port', '7071')
565
+ .option('--bff-port <port>', 'React managed BFF Functions port', '7073')
534
566
  .option('--host <host>', 'Host name', 'localhost')
535
567
  .option('--open', 'Open browser automatically', false)
536
568
  .option('--verbose', 'Show verbose logs', false)
537
569
  .option('--no-functions', 'Skip Azure Functions startup')
538
570
  .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
539
571
  .option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
540
- .option('--swa-port <port>', 'SWA authentication emulator port', '4280')
541
- .option('--no-swa', 'Skip the SWA authentication emulator')
572
+ .option('--swa-port <port>', 'SWA routing/authentication emulator port', '4280')
573
+ .option('--no-swa', 'Skip SWA routing/authentication emulation (React direct access has no same-origin API)')
542
574
  .action(async (options: ParsedDevActionOptions) => {
543
575
  const normalizedOptions = normalizeParsedDevOptions(options);
544
576
 
@@ -656,19 +688,24 @@ async function startDevEnvironment(options: DevOptions) {
656
688
  const port = options.port || '3000';
657
689
  const functionsPort = options.functionsPort || '7071';
658
690
  const swaPort = options.swaPort || '4280';
691
+ const bffPort = options.bffPort || '7073';
659
692
 
660
693
  // Detect package manager from project lockfile
661
694
  const pm = detectFromProject();
662
695
  const pmCmd = getCommands(pm);
663
696
  const backendLanguage = getBackendLanguage();
697
+ const react = getFrontendFramework() === 'react';
698
+ const frontendLabel = react ? 'React/Vite' : 'Next.js';
664
699
  const authConfig = getAuthConfig();
665
700
  const normalizedAuth = normalizeAuthConfig(authConfig);
666
- const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
701
+ const authSchemes = Object.values(normalizedAuth?.schemes ?? {});
702
+ const swaScheme = authSchemes.find(scheme => scheme.provider === 'swa');
703
+ const useSwaEmulator = (react || Boolean(swaScheme)) && !options.noSwa;
667
704
  const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
668
705
 
669
706
  if (useSwaEmulator && !swaCommand) {
670
707
  const installCommand = getSwaCliInstallCommand(pm);
671
- console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
708
+ console.error('❌ SWA routing/authentication requires Azure Static Web Apps CLI for local development.');
672
709
  console.error(' Install it in this project and retry:');
673
710
  console.error(`\n ${installCommand}\n`);
674
711
  process.exitCode = 1;
@@ -707,15 +744,22 @@ async function startDevEnvironment(options: DevOptions) {
707
744
  if (mockServer) {
708
745
  await mockServer.stop();
709
746
  }
710
- processes.forEach((proc) => {
711
- if (proc && !proc.killed) {
712
- proc.kill();
713
- }
714
- });
747
+ stopDevProcesses(processes);
715
748
  process.exit(0);
716
749
  });
717
750
 
718
751
  try {
752
+ if (react) {
753
+ const ports = [port, bffPort, ...(!options.noFunctions ? [functionsPort] : []), ...(useSwaEmulator ? [swaPort] : []), ...(options.mockConnectors ? [String(Number(functionsPort) + 1)] : [])];
754
+ if (ports.some(value => !/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535) || new Set(ports).size !== ports.length) {
755
+ throw new Error('Frontend, BFF, standalone Functions, mock, and SWA ports must be valid and distinct');
756
+ }
757
+ for (const value of ports) await assertDevPortAvailable(Number(value), options.host || 'localhost');
758
+ const coreToolsVersion = await resolveInstalledCoreToolsVersion();
759
+ if (!coreToolsVersion || compareVersionNumbers(coreToolsVersion, '4.0.0') < 0) {
760
+ throw new Error('React BFF requires Azure Functions Core Tools v4. Install it explicitly before running dev.');
761
+ }
762
+ }
719
763
  // 1. Check for Next.js project
720
764
  const packageJsonPath = path.join(process.cwd(), 'package.json');
721
765
  const nextConfigPathJs = path.join(process.cwd(), 'next.config.js');
@@ -724,11 +768,11 @@ async function startDevEnvironment(options: DevOptions) {
724
768
 
725
769
  if (!fs.existsSync(packageJsonPath)) {
726
770
  console.log('❌ package.json not found.');
727
- console.log('💡 Please run this command in the root directory of a Next.js project.');
771
+ console.log('💡 Please run this command in the root directory of a SwallowKit project.');
728
772
  process.exit(1);
729
773
  }
730
774
 
731
- if (!fs.existsSync(nextConfigPathJs) && !fs.existsSync(nextConfigPathTs) && !fs.existsSync(nextConfigPathMjs)) {
775
+ if (!react && !fs.existsSync(nextConfigPathJs) && !fs.existsSync(nextConfigPathTs) && !fs.existsSync(nextConfigPathMjs)) {
732
776
  console.log('⚠️ next.config file not found. Is this a Next.js project?');
733
777
  }
734
778
 
@@ -963,14 +1007,14 @@ async function startDevEnvironment(options: DevOptions) {
963
1007
  );
964
1008
  } else if (!hasFunctions) {
965
1009
  console.log('');
966
- console.log('ℹ️ functions/ directory not found. Starting Next.js only.');
1010
+ console.log(`ℹ️ functions/ directory not found. Starting ${frontendLabel}${react ? ' and managed BFF' : ''} only.`);
967
1011
  } else if (options.noFunctions) {
968
1012
  console.log('');
969
1013
  console.log('ℹ️ --no-functions specified. Skipping Azure Functions.');
970
1014
  }
971
1015
 
972
1016
  console.log('');
973
- console.log('🚀 Starting Next.js development server...');
1017
+ console.log(`🚀 Starting ${frontendLabel} development server...`);
974
1018
 
975
1019
  // Mock connector server — start proxy if --mock-connectors is enabled
976
1020
  let bffTargetPort = functionsPort;
@@ -1039,7 +1083,7 @@ async function startDevEnvironment(options: DevOptions) {
1039
1083
  }
1040
1084
 
1041
1085
  // 5. Start Next.js development server
1042
- const nextArgs = buildNextDevArgs(pm, port);
1086
+ const nextArgs = react ? buildReactDevArgs(pm, port, options.host) : buildNextDevArgs(pm, port);
1043
1087
 
1044
1088
  // Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
1045
1089
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
@@ -1067,6 +1111,25 @@ async function startDevEnvironment(options: DevOptions) {
1067
1111
  FUNCTIONS_BASE_URL: bffTargetUrl,
1068
1112
  };
1069
1113
 
1114
+ if (react) {
1115
+ await runCommand(pm, ['run', 'build'], path.join(process.cwd(), 'shared'), 'Shared schema build');
1116
+ const bffDir = path.join(process.cwd(), 'bff');
1117
+ await runCommand(pm, ['run', 'build'], bffDir, 'Managed BFF build');
1118
+ const bffProcess = spawn('func', buildFunctionsStartArgs(bffPort), {
1119
+ cwd: bffDir, shell: true, stdio: 'inherit', env: nextEnv,
1120
+ });
1121
+ processes.push(bffProcess);
1122
+ bffProcess.on('error', error => console.error('❌ Managed BFF startup error:', error.message));
1123
+ bffProcess.on('close', code => {
1124
+ console.error(`⏹️ Managed BFF exited (exit code: ${code})`);
1125
+ stopDevProcesses(processes);
1126
+ process.exit(code || 1);
1127
+ });
1128
+ if (!await waitForHttpServerReady(buildFunctionsBaseUrl(options.host, bffPort))) {
1129
+ throw new Error(`Managed BFF did not become ready on port ${bffPort}`);
1130
+ }
1131
+ }
1132
+
1070
1133
  const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
1071
1134
  cwd: process.cwd(),
1072
1135
  shell: true,
@@ -1077,23 +1140,19 @@ async function startDevEnvironment(options: DevOptions) {
1077
1140
  processes.push(nextProcess);
1078
1141
 
1079
1142
  nextProcess.on('error', (error) => {
1080
- console.error('❌ Next.js startup error:', error.message);
1143
+ console.error(`❌ ${frontendLabel} startup error:`, error.message);
1081
1144
  process.exit(1);
1082
1145
  });
1083
1146
 
1084
1147
  nextProcess.on('close', (code) => {
1085
1148
  if (code !== 0) {
1086
- console.log(`\n⏹️ Next.js exited (exit code: ${code})`);
1149
+ console.log(`\n⏹️ ${frontendLabel} exited (exit code: ${code})`);
1087
1150
  }
1088
- // Exit all processes when Next.js exits
1151
+ // Exit all owned processes when the frontend exits.
1089
1152
  if (mockServer) {
1090
1153
  mockServer.stop().catch(() => {});
1091
1154
  }
1092
- processes.forEach((proc) => {
1093
- if (proc && !proc.killed) {
1094
- proc.kill();
1095
- }
1096
- });
1155
+ stopDevProcesses(processes);
1097
1156
  process.exit(code || 0);
1098
1157
  });
1099
1158
 
@@ -1110,14 +1169,14 @@ async function startDevEnvironment(options: DevOptions) {
1110
1169
  let swaReady = false;
1111
1170
  if (useSwaEmulator && swaCommand) {
1112
1171
  const nextUrl = `http://${options.host || 'localhost'}:${port}`;
1113
- console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
1172
+ console.log(`⏳ Waiting for ${frontendLabel} before starting the SWA emulator at ${nextUrl}...`);
1114
1173
  const nextReady = await waitForHttpServerReady(nextUrl);
1115
1174
  if (!nextReady) {
1116
- throw new Error(`Next.js did not become ready at ${nextUrl}`);
1175
+ throw new Error(`${frontendLabel} did not become ready at ${nextUrl}`);
1117
1176
  }
1118
1177
 
1119
- console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
1120
- const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
1178
+ console.log(`🔐 Starting SWA routing/authentication emulator (port: ${swaPort})...`);
1179
+ const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort, react ? bffPort : undefined), {
1121
1180
  cwd: process.cwd(),
1122
1181
  shell: true,
1123
1182
  stdio: 'inherit',
@@ -1131,7 +1190,7 @@ async function startDevEnvironment(options: DevOptions) {
1131
1190
  const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1132
1191
  swaReady = await waitForHttpServerReady(swaUrl);
1133
1192
  if (!swaReady) {
1134
- throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
1193
+ throw new Error(`SWA routing/authentication emulator did not become ready at ${swaUrl}`);
1135
1194
  }
1136
1195
  }
1137
1196
 
@@ -1140,18 +1199,25 @@ async function startDevEnvironment(options: DevOptions) {
1140
1199
  console.log('');
1141
1200
  if (useSwaEmulator) {
1142
1201
  const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
1143
- console.log('🔐 SWA authenticated app:');
1202
+ console.log('🔐 SWA app (same-origin frontend and BFF):');
1144
1203
  console.log(` ${swaUrl}`);
1145
1204
  console.log('');
1146
- console.log('🔑 Local sign-in:');
1147
- console.log(` ${swaUrl}/.auth/login/aad`);
1148
- console.log(' Enter any username and select Login.');
1149
- console.log(' The "authenticated" role is added automatically.');
1150
- console.log('');
1151
- console.log('📱 Next.js direct access (authentication is not emulated):');
1205
+ if (swaScheme) {
1206
+ const provider = swaScheme.swa?.allowedProviders?.[0] || 'aad';
1207
+ console.log('🔑 Local SWA sign-in:');
1208
+ console.log(` ${swaUrl}/.auth/login/${provider}`);
1209
+ console.log(' Enter any username and select Login.');
1210
+ console.log(' The "authenticated" role is added automatically.');
1211
+ console.log('');
1212
+ }
1213
+ if (authSchemes.some(scheme => scheme.provider === 'custom-jwt')) {
1214
+ console.log(`🔑 Application sign-in: ${swaUrl}/login`);
1215
+ console.log('');
1216
+ }
1217
+ console.log(`📱 ${frontendLabel} direct access (SWA routing/authentication is not emulated):`);
1152
1218
  console.log(` http://${options.host || 'localhost'}:${port}`);
1153
1219
  } else {
1154
- console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
1220
+ console.log(`📱 ${frontendLabel}: http://${options.host || 'localhost'}:${port}`);
1155
1221
  }
1156
1222
  if (hasFunctions && !options.noFunctions) {
1157
1223
  console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
@@ -1161,7 +1227,7 @@ async function startDevEnvironment(options: DevOptions) {
1161
1227
  }
1162
1228
  console.log('');
1163
1229
  if (hasFunctions && !options.noFunctions && functionsReady) {
1164
- console.log('💡 Azure Functions and Next.js BFF are connected');
1230
+ console.log(`💡 Azure Functions and ${react ? 'managed Functions' : 'Next.js'} BFF are connected`);
1165
1231
  } else if (hasFunctions && !options.noFunctions) {
1166
1232
  console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
1167
1233
  }
@@ -1171,6 +1237,7 @@ async function startDevEnvironment(options: DevOptions) {
1171
1237
  if (swaReady) {
1172
1238
  console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
1173
1239
  }
1240
+ if (react) console.log(`🔀 Managed BFF: ${buildFunctionsBaseUrl(options.host, bffPort)} (not the standalone Functions endpoint)`);
1174
1241
  console.log('');
1175
1242
 
1176
1243
  if (options.open) {
@@ -1189,11 +1256,7 @@ async function startDevEnvironment(options: DevOptions) {
1189
1256
 
1190
1257
  } catch (error) {
1191
1258
  console.error('❌ Failed to start development environment:', error instanceof Error ? error.message : error);
1192
- processes.forEach((proc) => {
1193
- if (proc && !proc.killed) {
1194
- proc.kill();
1195
- }
1196
- });
1259
+ stopDevProcesses(processes);
1197
1260
  process.exit(1);
1198
1261
  }
1199
1262
  }