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
@@ -2,7 +2,8 @@
2
2
  import * as path from "path";
3
3
  import { spawn, execSync } from "child_process";
4
4
  import prompts from "prompts";
5
- import { BackendLanguage } from "../../types";
5
+ import { BackendLanguage, FrontendFramework } from "../../types";
6
+ import { buildReactReadme, buildReactSwaConfig, createReactBffProject, createReactProject, buildReactGitHubSwaWorkflow, buildReactAzureSwaPipeline } from "../../core/project/react-project";
6
7
  import {
7
8
  type PackageManager,
8
9
  detectFromUserAgent,
@@ -29,6 +30,7 @@ import { getProjectLocalUvPaths } from "../../utils/python-uv";
29
30
  interface InitOptions {
30
31
  name: string;
31
32
  template: string;
33
+ frontend?: FrontendFramework;
32
34
  nextVersion?: string;
33
35
  cicd?: CiCdProvider;
34
36
  backendLanguage?: BackendLanguage;
@@ -257,6 +259,9 @@ const VALID_VNET: VNetOption[] = ['none', 'outbound'];
257
259
  const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
258
260
 
259
261
  function validateInitFlags(options: InitOptions): void {
262
+ if (options.frontend && !["nextjs", "react"].includes(options.frontend)) {
263
+ throw new Error("Invalid --frontend: expected nextjs or react");
264
+ }
260
265
  if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
261
266
  throw new Error('Invalid --package-manager: expected npm or pnpm');
262
267
  }
@@ -322,17 +327,28 @@ export async function initCommand(options: InitOptions) {
322
327
  }
323
328
  }
324
329
 
330
+ // Preserve existing non-interactive invocations while offering a frontend choice in the wizard.
331
+ const frontend: FrontendFramework = options.frontend ?? (interactive
332
+ ? (await prompts({
333
+ type: "select", name: "frontend", message: "Frontend framework:",
334
+ choices: [{ title: "Next.js (App Router)", value: "nextjs" }, { title: "React (Vite SPA + SWA managed Functions BFF)", value: "react" }],
335
+ initial: 0,
336
+ })).frontend || "nextjs"
337
+ : "nextjs");
338
+ options = { ...options, frontend };
339
+
325
340
  // Use flag values if provided, otherwise prompt interactively
326
341
  const cicdProvider: CiCdProvider = options.cicd || await promptCiCd();
327
342
  const backendLanguage: BackendLanguage = options.backendLanguage || await promptBackendLanguage();
328
343
 
329
344
  const azureConfig = await promptAzureConfig(options);
330
345
 
331
- // Create Next.js project with create-next-app
332
- await createNextJsProject(options.name, pm);
333
-
334
- // Upgrade Next.js to specified version (or latest) to avoid cached old versions
335
- await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
346
+ if (frontend === "react") {
347
+ createReactProject(projectDir, options.name);
348
+ } else {
349
+ await createNextJsProject(options.name, pm);
350
+ await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
351
+ }
336
352
 
337
353
  // Add SwallowKit specific files
338
354
  await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
@@ -342,9 +358,9 @@ export async function initCommand(options: InitOptions) {
342
358
 
343
359
  // Create CI/CD files based on choice
344
360
  if (cicdProvider === 'github') {
345
- await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
361
+ await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage, frontend);
346
362
  } else if (cicdProvider === 'azure') {
347
- await createAzurePipelines(projectDir, pm, backendLanguage);
363
+ await createAzurePipelines(projectDir, pm, backendLanguage, frontend);
348
364
  }
349
365
 
350
366
  await syncProjectManifest(projectDir);
@@ -651,9 +667,13 @@ export function parseIgnoredBuilds(output: string): string[] {
651
667
  }
652
668
 
653
669
  export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
654
- return nextConfigContent.replace(
670
+ const compatibleConfig = nextConfigContent.replace(/(\b(?:cacheComponents|partialPrefetching)\s*:\s*)true\b/g, '$1false');
671
+ const cacheComponentsSetting = /\bcacheComponents\s*:/.test(compatibleConfig)
672
+ ? ''
673
+ : ' cacheComponents: false,\n';
674
+ return compatibleConfig.replace(
655
675
  /(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
656
- `$1\n output: 'standalone',\n agentRules: false, // SwallowKit owns the canonical root AGENTS.md contract\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
676
+ `$1\n output: 'standalone',\n${cacheComponentsSetting} agentRules: false, // SwallowKit owns the canonical root AGENTS.md contract\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
657
677
  );
658
678
  }
659
679
 
@@ -704,8 +724,11 @@ export function buildCSharpFunctionsProjectSource(): string {
704
724
  `;
705
725
  }
706
726
 
707
- export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): string {
727
+ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage, frontend: FrontendFramework = "nextjs"): string {
708
728
  return `module.exports = {
729
+ frontend: {
730
+ framework: '${frontend}',
731
+ },
709
732
  backend: {
710
733
  language: '${backendLanguage}',
711
734
  },
@@ -723,10 +746,10 @@ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): s
723
746
  export function buildGeneratedProjectDependencies(
724
747
  projectName: string,
725
748
  pm: PackageManager = 'pnpm',
749
+ frontend: FrontendFramework = "nextjs",
726
750
  ): Record<string, string> {
727
751
  return {
728
- '@azure/cosmos': '^4.0.0',
729
- 'applicationinsights': '^3.3.0',
752
+ ...(frontend === "nextjs" ? { '@azure/cosmos': '^4.0.0', 'applicationinsights': '^3.3.0' } : {}),
730
753
  'zod': '^4.0.0',
731
754
  [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
732
755
  };
@@ -795,6 +818,7 @@ async function addSwallowKitFiles(
795
818
  console.log('📦 Adding SwallowKit files...\n');
796
819
 
797
820
  const projectName = options.name;
821
+ const frontend = options.frontend ?? "nextjs";
798
822
 
799
823
  // 1. Update package.json to add runtime dependencies for generated projects
800
824
  const packageJsonPath = path.join(projectDir, 'package.json');
@@ -803,7 +827,7 @@ async function addSwallowKitFiles(
803
827
  // Root BFF/auth code imports Zod directly; declare it as well as the shared package.
804
828
  packageJson.dependencies = {
805
829
  ...packageJson.dependencies,
806
- ...buildGeneratedProjectDependencies(projectName, pm),
830
+ ...buildGeneratedProjectDependencies(projectName, pm, frontend),
807
831
  };
808
832
  packageJson.devDependencies = {
809
833
  ...packageJson.devDependencies,
@@ -812,8 +836,12 @@ async function addSwallowKitFiles(
812
836
 
813
837
  packageJson.scripts = {
814
838
  ...packageJson.scripts,
815
- 'build': getBuildScript(pm),
816
- 'start': 'next start',
839
+ 'build': getBuildScript(pm, frontend),
840
+ 'start': frontend === "react" ? "vite preview" : 'next start',
841
+ ...(frontend === "react" ? {
842
+ typecheck: `tsc --noEmit && ${getCommands(pm).exec} tsc --noEmit -p bff`,
843
+ "bff:start": `${getCommands(pm).runFilter("bff")} start`,
844
+ } : {}),
817
845
  'functions:start': getFunctionsStartScript(pm, backendLanguage),
818
846
  };
819
847
 
@@ -827,7 +855,7 @@ async function addSwallowKitFiles(
827
855
  delete packageJson.packageManager;
828
856
 
829
857
  // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
830
- const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
858
+ const workspacePackages = [...(usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared']), ...(frontend === "react" ? ["bff"] : [])];
831
859
  const wsConfig = getWorkspaceConfig(pm, workspacePackages);
832
860
  packageJson.workspaces = workspacePackages;
833
861
 
@@ -839,7 +867,7 @@ async function addSwallowKitFiles(
839
867
 
840
868
  // Create pnpm-workspace.yaml for local pnpm development when needed.
841
869
  if (wsConfig.type === 'file') {
842
- fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
870
+ fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content + (frontend === "react" ? " esbuild: true\n" : ""));
843
871
  }
844
872
 
845
873
  // Don't install yet — wait until all workspace packages (shared, functions) are created
@@ -878,12 +906,13 @@ async function addSwallowKitFiles(
878
906
  }
879
907
 
880
908
  // 3. Create SwallowKit config
881
- const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
909
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage, frontend);
882
910
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
883
911
 
884
912
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
885
913
  await createSharedPackage(projectDir, projectName);
886
914
 
915
+ if (frontend === "nextjs") {
887
916
  // Create lib directory for Next.js-specific utilities
888
917
  const libDir = path.join(projectDir, 'lib');
889
918
 
@@ -1003,6 +1032,7 @@ export const api = {
1003
1032
  };
1004
1033
  `;
1005
1034
  fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
1035
+ }
1006
1036
 
1007
1037
  // 5. Create components directory
1008
1038
  const componentsDir = path.join(projectDir, 'components');
@@ -1091,7 +1121,7 @@ export async function register() {
1091
1121
  }
1092
1122
  }
1093
1123
  `;
1094
- fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
1124
+ if (frontend === "nextjs") fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
1095
1125
 
1096
1126
  // 8. Create .env.local for local development
1097
1127
  const envLocalContent = [
@@ -1124,17 +1154,19 @@ export async function register() {
1124
1154
  };
1125
1155
  fs.writeFileSync(
1126
1156
  path.join(projectDir, 'staticwebapp.config.json'),
1127
- JSON.stringify(swaConfig, null, 2)
1157
+ JSON.stringify(frontend === "react" ? buildReactSwaConfig() : swaConfig, null, 2)
1128
1158
  );
1129
1159
 
1130
1160
  // 14. Create Azure Functions project
1131
1161
  await createAzureFunctionsProject(projectDir, pm, backendLanguage);
1132
1162
 
1133
1163
  // 15. Create BFF API route to call Azure Functions
1134
- await createBffApiRoute(projectDir);
1135
-
1136
- // 16. Create home page
1137
- await createHomePage(projectDir, pm);
1164
+ if (frontend === "react") {
1165
+ createReactBffProject(projectDir, projectName, pm);
1166
+ } else {
1167
+ await createBffApiRoute(projectDir);
1168
+ await createHomePage(projectDir, pm);
1169
+ }
1138
1170
 
1139
1171
  // 17. Install all workspace dependencies (root + shared + functions)
1140
1172
  console.log('📦 Installing workspace dependencies...\n');
@@ -1143,10 +1175,14 @@ export async function register() {
1143
1175
  console.log('✅ Project structure created\n');
1144
1176
 
1145
1177
  // 18. Create README.md
1146
- createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
1178
+ if (frontend === "react") {
1179
+ fs.writeFileSync(path.join(projectDir, "README.md"), buildReactReadme(projectName, pm, backendLanguage));
1180
+ } else {
1181
+ createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
1182
+ }
1147
1183
 
1148
1184
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
1149
- createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
1185
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm, frontend);
1150
1186
  }
1151
1187
 
1152
1188
  async function createSharedPackage(projectDir: string, projectName: string) {
@@ -1978,7 +2014,8 @@ function createAiAgentFiles(
1978
2014
  projectDir: string,
1979
2015
  projectName: string,
1980
2016
  backendLanguage: BackendLanguage,
1981
- pm: PackageManager
2017
+ pm: PackageManager,
2018
+ frontend: FrontendFramework = "nextjs"
1982
2019
  ) {
1983
2020
  console.log('🤖 Creating AI agent instruction files...\n');
1984
2021
  const runCmd = getCommands(pm).exec;
@@ -1987,6 +2024,7 @@ function createAiAgentFiles(
1987
2024
  projectName,
1988
2025
  backendLanguage,
1989
2026
  packageManager: pm,
2027
+ frontend,
1990
2028
  });
1991
2029
  console.log(' ✅ AGENTS.md (canonical project contract)');
1992
2030
  console.log(' ✅ CLAUDE.md (Claude Code adapter)');
@@ -3133,7 +3171,8 @@ export function getSwaNodeVersionCommand(): string {
3133
3171
  return `node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.engines={...p.engines,node:'>=22.14.0 <23'};fs.writeFileSync('package.json',JSON.stringify(p,null,2)+String.fromCharCode(10));"`;
3134
3172
  }
3135
3173
 
3136
- export function buildAzureSwaPipeline(pm: PackageManager, projectPm: PackageManager = pm): string {
3174
+ export function buildAzureSwaPipeline(pm: PackageManager, projectPm: PackageManager = pm, frontend: FrontendFramework = "nextjs"): string {
3175
+ if (frontend === "react") return buildReactAzureSwaPipeline(projectPm);
3137
3176
  // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
3138
3177
  pm = 'npm';
3139
3178
  const pmCmd = getCommands(pm);
@@ -3321,7 +3360,8 @@ async function createGitHubActionsWorkflows(
3321
3360
  projectDir: string,
3322
3361
  azureConfig: AzureConfig,
3323
3362
  pm: PackageManager,
3324
- backendLanguage: BackendLanguage
3363
+ backendLanguage: BackendLanguage,
3364
+ frontend: FrontendFramework = "nextjs"
3325
3365
  ) {
3326
3366
  console.log('📦 Creating GitHub Actions workflows...\n');
3327
3367
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
@@ -3329,7 +3369,7 @@ async function createGitHubActionsWorkflows(
3329
3369
 
3330
3370
  const actionsPm: PackageManager = pm;
3331
3371
 
3332
- const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3372
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm, frontend);
3333
3373
  fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3334
3374
 
3335
3375
  // deploy-functions.yml
@@ -3339,7 +3379,8 @@ async function createGitHubActionsWorkflows(
3339
3379
  console.log('✅ GitHub Actions workflows created\n');
3340
3380
  }
3341
3381
 
3342
- export function buildGitHubSwaWorkflow(pm: PackageManager, projectPm: PackageManager = pm): string {
3382
+ export function buildGitHubSwaWorkflow(pm: PackageManager, projectPm: PackageManager = pm, frontend: FrontendFramework = "nextjs"): string {
3383
+ if (frontend === "react") return buildReactGitHubSwaWorkflow(projectPm);
3343
3384
  // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
3344
3385
  pm = 'npm';
3345
3386
  const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
@@ -3417,7 +3458,7 @@ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3417
3458
  `;
3418
3459
  }
3419
3460
 
3420
- async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
3461
+ async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage, frontend: FrontendFramework = "nextjs") {
3421
3462
  console.log('📦 Creating Azure Pipelines...\n');
3422
3463
 
3423
3464
  const pipelinesPm: PackageManager = pm;
@@ -3425,7 +3466,7 @@ async function createAzurePipelines(projectDir: string, pm: PackageManager, back
3425
3466
  fs.mkdirSync(pipelinesDir, { recursive: true });
3426
3467
 
3427
3468
  // swa.yml
3428
- const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3469
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm, frontend);
3429
3470
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3430
3471
 
3431
3472
  // functions.yml
@@ -3434,4 +3475,3 @@ async function createAzurePipelines(projectDir: string, pm: PackageManager, back
3434
3475
 
3435
3476
  console.log('✅ Azure Pipelines created\n');
3436
3477
  }
3437
-
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * SwallowKit Scaffold コマンド
3
- * Zod モデルから Azure Functions と Next.js BFF の CRUD コードを生成
3
+ * Zod モデルから Azure Functions、BFF、選択したフロントエンドの CRUD を生成
4
4
  */
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import { spawnSync } from "child_process";
9
- import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
10
- import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
9
+ import { getBackendLanguage, getFrontendFramework, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
10
+ import { generateManagedFunctionConfig, generateReactBffHelper, generateReactBffRoute } from "../../core/scaffold/react-bff-generator";
11
+ import { adaptReactPage, generateReactModelRoutes } from "../../core/scaffold/react-ui-generator";
12
+ import { ModelInfo, parseModelFile, toKebabCase, toCamelCase } from "../../core/scaffold/model-parser";
11
13
  import { MachineCommandError } from "../../machine/errors";
12
14
  import {
13
15
  generateCSharpAzureFunctionsCRUD,
@@ -58,7 +60,7 @@ const SCAFFOLD_GENERATOR = "scaffold";
58
60
  interface ScaffoldOptions {
59
61
  model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
60
62
  functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
61
- apiDir?: string; // Next.js API routes のディレクトリ(デフォルト: "app/api")
63
+ apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
62
64
  apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
63
65
  expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
64
66
  }
@@ -131,6 +133,8 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
131
133
  console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
132
134
 
133
135
  const backendLanguage = getBackendLanguage();
136
+ const frontend = getFrontendFramework();
137
+ const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
134
138
  console.log(`🧠 Backend language: ${backendLanguage}`);
135
139
 
136
140
  // コネクタモデルかどうかを判定
@@ -182,7 +186,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
182
186
  }
183
187
 
184
188
  // 5. Generate BFF callFunction helper
185
- await generateCallFunctionHelper(session);
189
+ await generateCallFunctionHelper(session, apiDir);
186
190
 
187
191
  // 6. Generate Azure Functions code
188
192
  if (isConnectorModel) {
@@ -197,9 +201,10 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
197
201
  }
198
202
  }
199
203
 
200
- // 7. Generate Next.js BFF API Routes
201
- const apiDir = options.apiDir || "app/api";
202
- if (isConnectorModel) {
204
+ // 7. Generate framework-specific BFF routes (the standalone backend is unchanged).
205
+ if (frontend === "react") {
206
+ generateManagedBffRoutes(modelInfo, apiDir, sharedPackageName, session);
207
+ } else if (isConnectorModel) {
203
208
  await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
204
209
  } else {
205
210
  await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
@@ -237,14 +242,14 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
237
242
  console.log("\n📝 Next steps:");
238
243
  console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
239
244
  if (!options.apiOnly) {
240
- console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
245
+ console.log(` 2. Check the generated UI pages in ${frontend === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`);
241
246
  console.log(" 3. Navigate to the model from the homepage menu");
242
247
  }
243
248
  if (backendLanguage !== "typescript") {
244
249
  console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
245
250
  }
246
251
  console.log(
247
- ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
252
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in ${frontend === "react" ? `${apiDir}/local.settings.json` : "your .env.local file"}`
248
253
  );
249
254
  console.log(
250
255
  ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
@@ -258,7 +263,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
258
263
  }
259
264
 
260
265
  function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
261
- const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
266
+ const uiPrefix = `${getFrontendFramework() === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`;
262
267
  for (const operation of session.operations) {
263
268
  if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
264
269
  let content = session.readFile(operation.absolutePath);
@@ -384,10 +389,17 @@ function readSharedPackageName(): string {
384
389
  /**
385
390
  * Generate BFF callFunction helper (lib/api/call-function.ts)
386
391
  */
387
- async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
392
+ async function generateCallFunctionHelper(session: FileOperationSession, apiDir: string): Promise<void> {
388
393
  console.log("\n📦 Generating BFF callFunction helper...");
389
394
 
390
395
  const cwd = process.cwd();
396
+ if (getFrontendFramework() === "react") {
397
+ const projectName = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).name;
398
+ session.writeFile(path.join(cwd, apiDir, "src", "lib", "call-function.ts"), generateReactBffHelper(projectName, getAuthConfig()), {
399
+ ownership: "managed", generator: SCAFFOLD_GENERATOR,
400
+ });
401
+ return;
402
+ }
391
403
 
392
404
  const helperDir = path.join(cwd, "lib", "api");
393
405
  const helperPath = path.join(helperDir, "call-function.ts");
@@ -800,6 +812,13 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
800
812
  /**
801
813
  * Next.js BFF API Routes を生成
802
814
  */
815
+ function generateManagedBffRoutes(modelInfo: ModelInfo, apiDir: string, sharedPackageName: string, session: FileOperationSession): void {
816
+ const name = toCamelCase(modelInfo.name);
817
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
818
+ session.writeFile(path.join(process.cwd(), apiDir, "src", name, "index.ts"), generateReactBffRoute(modelInfo, sharedPackageName), meta);
819
+ session.writeFile(path.join(process.cwd(), apiDir, name, "function.json"), generateManagedFunctionConfig(`${name}/{id?}`, ["GET", "POST", "PUT", "DELETE"], name), meta);
820
+ }
821
+
803
822
  async function generateBFFRoutes(
804
823
  modelInfo: any,
805
824
  apiDir: string,
@@ -867,9 +886,10 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
867
886
  const modelKebab = toKebabCase(modelInfo.name);
868
887
  const modelName = modelInfo.name;
869
888
  const cwd = process.cwd();
889
+ const react = getFrontendFramework() === "react";
870
890
 
871
891
  // Create directory structure: app/[model]/
872
- const modelDir = path.join(cwd, "app", modelKebab);
892
+ const modelDir = path.join(cwd, react ? path.join("src", "pages") : "app", modelKebab);
873
893
  const componentsDir = path.join(modelDir, "_components");
874
894
  const newDir = path.join(modelDir, "new");
875
895
  const idDir = path.join(modelDir, "[id]");
@@ -884,14 +904,16 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
884
904
  const authLayout = generateAuthLayout(modelInfo, authOptions);
885
905
 
886
906
  const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
887
- session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
888
- session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
889
- session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
890
- session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
891
- session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
907
+ const adapt = react ? adaptReactPage : (source: string) => source;
908
+ session.writeFile(path.join(modelDir, "page.tsx"), adapt(listPage), meta);
909
+ session.writeFile(path.join(idDir, "page.tsx"), adapt(detailPage), meta);
910
+ session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), adapt(formComponent), meta);
911
+ session.writeFile(path.join(newDir, "page.tsx"), adapt(newPage), meta);
912
+ session.writeFile(path.join(editDir, "page.tsx"), adapt(editPage), meta);
892
913
  if (authLayout) {
893
914
  session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
894
915
  }
916
+ if (react) session.writeFile(path.join(modelDir, "routes.tsx"), generateReactModelRoutes(modelInfo, authOptions), meta);
895
917
 
896
918
  console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
897
919
  console.log(`✅ Created: ${path.join(idDir, "page.tsx")}`);
@@ -971,4 +993,3 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
971
993
  session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
972
994
  console.log(`✅ Added ${modelInfo.name} to navigation menu`);
973
995
  }
974
-
package/src/cli/index.ts CHANGED
@@ -24,6 +24,7 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
24
24
  ["--port", 1],
25
25
  ["-f", 1],
26
26
  ["--functions-port", 1],
27
+ ["--bff-port", 1],
27
28
  ["--host", 1],
28
29
  ["--seed-env", 1],
29
30
  ["-o", 0],
@@ -102,7 +103,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
102
103
 
103
104
  program
104
105
  .name("swallowkit")
105
- .description("Next.js framework optimized for Azure deployment - Automatically splits SSR into individual Azure Functions")
106
+ .description("Schema-driven React and Next.js applications on Azure Static Web Apps and Functions")
106
107
  .version(CLI_VERSION);
107
108
 
108
109
  // Register commands
@@ -110,6 +111,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
110
111
  .command("init [project-name]")
111
112
  .description("Initialize a new SwallowKit project")
112
113
  .option("--template <template>", "Template to use", "default")
114
+ .option("--frontend <framework>", "Frontend framework: nextjs | react (defaults to nextjs in non-interactive mode)")
113
115
  .option("--next-version <version>", "Next.js version to install (e.g., 16.0.7, latest)", "latest")
114
116
  .option("--package-manager <manager>", "Package manager: npm | pnpm (defaults to invocation context)")
115
117
  .option("--cicd <provider>", "CI/CD provider: github | azure | skip")
@@ -121,6 +123,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
121
123
  initCommand({
122
124
  name: projectName || "swallowkit-app",
123
125
  template: options.template,
126
+ frontend: options.frontend,
124
127
  nextVersion: options.nextVersion,
125
128
  packageManager: options.packageManager,
126
129
  cicd: options.cicd,
@@ -153,9 +156,9 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
153
156
 
154
157
  program
155
158
  .command("scaffold <model>")
156
- .description("Generate CRUD code for Azure Functions and Next.js BFF from Zod models")
159
+ .description("Generate CRUD code for Azure Functions, BFF, and the selected frontend from Zod models")
157
160
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
158
- .option("--api-dir <dir>", "Next.js API routes directory", "app/api")
161
+ .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
159
162
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
160
163
  .option("--dry-run", "Show planned file changes without writing anything", false)
161
164
  .action(async (model, options) => {
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import { createRequire } from "module";
3
3
  import * as path from "path";
4
- import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
4
+ import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, FrontendFramework, NormalizedAuthConfig, SwallowKitConfig } from "../types";
5
5
  import { detectFromProject, getCommands } from "../utils/package-manager";
6
6
 
7
7
  const requireFromHere = createRequire(__filename);
@@ -21,6 +21,7 @@ const VALID_BACKEND_LANGUAGES: BackendLanguage[] = ["typescript", "csharp", "pyt
21
21
  * デフォルト設定
22
22
  */
23
23
  const DEFAULT_CONFIG: SwallowKitConfig = {
24
+ frontend: { framework: "nextjs" },
24
25
  database: {},
25
26
  backend: {
26
27
  language: "typescript",
@@ -78,6 +79,10 @@ export function loadConfig(configPath?: string, throwOnError = false, quiet = fa
78
79
  */
79
80
  function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<SwallowKitConfig>): SwallowKitConfig {
80
81
  return {
82
+ frontend: {
83
+ ...defaultConfig.frontend,
84
+ ...userConfig.frontend,
85
+ },
81
86
  database: {
82
87
  ...defaultConfig.database,
83
88
  ...userConfig.database,
@@ -108,6 +113,9 @@ function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<Swallo
108
113
  export function generateConfig(outputPath: string = "swallowkit.config.json"): void {
109
114
  const config = {
110
115
  $schema: "https://swallowkit.dev/schema.json",
116
+ frontend: {
117
+ framework: "nextjs",
118
+ },
111
119
  database: {
112
120
  connectionString: "your-cosmos-connection-string",
113
121
  databaseName: "SwallowKitDB",
@@ -176,6 +184,14 @@ export function getBackendLanguage(configPath?: string): BackendLanguage {
176
184
  return "typescript";
177
185
  }
178
186
 
187
+ export function getFrontendFramework(configPath?: string): FrontendFramework {
188
+ const framework = getFullConfig(configPath).frontend?.framework ?? "nextjs";
189
+ if (framework !== "nextjs" && framework !== "react") {
190
+ throw new Error(`Unsupported frontend framework '${framework}'; use nextjs or react`);
191
+ }
192
+ return framework;
193
+ }
194
+
179
195
  /**
180
196
  * 完全な設定を取得(ファイル + 環境変数)
181
197
  */
@@ -250,6 +266,10 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
250
266
  export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
251
267
  const errors: string[] = [];
252
268
 
269
+ if (config.frontend?.framework && !["nextjs", "react"].includes(config.frontend.framework)) {
270
+ errors.push("Frontend framework must be one of: nextjs, react");
271
+ }
272
+
253
273
  const auth = normalizeAuthConfig(config.auth);
254
274
  if (auth) {
255
275
  for (const [name, scheme] of Object.entries(auth.schemes)) {
@@ -1,12 +1,13 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { BackendLanguage } from "../../types";
3
+ import { BackendLanguage, FrontendFramework } from "../../types";
4
4
  import { PackageManager, getCommands } from "../../utils/package-manager";
5
5
 
6
6
  export interface AgentInstructionContext {
7
7
  projectName: string;
8
8
  backendLanguage: BackendLanguage;
9
9
  packageManager: PackageManager;
10
+ frontend?: FrontendFramework;
10
11
  }
11
12
 
12
13
  export interface AgentInstructionDocuments {
@@ -75,11 +76,23 @@ function getBackendRules(backendLanguage: BackendLanguage): string {
75
76
 
76
77
  function interpolateTemplate(template: string, context: AgentInstructionContext): string {
77
78
  const runCmd = getCommands(context.packageManager).exec;
79
+ const react = context.frontend === "react";
78
80
  const replacements: Record<string, string> = {
79
81
  projectName: context.projectName,
80
82
  backendLanguageLabel: BACKEND_LANGUAGE_LABELS[context.backendLanguage],
81
83
  functionsStructureLine: getFunctionsStructureLine(context.backendLanguage),
82
84
  backendRules: getBackendRules(context.backendLanguage),
85
+ frontendLabel: react ? "React + Vite SPA" : "Next.js App Router",
86
+ bffLabel: react ? "SWA managed Functions" : "Next.js API routes",
87
+ bffPath: react ? "bff/src" : "app/api",
88
+ bffGlob: react ? "bff/**" : "app/api/**",
89
+ helperImport: react ? "../lib/call-function" : "@/lib/api/call-function",
90
+ frontendStructure: react
91
+ ? "├── src/ # React SPA (Vite + React Router)\n│ └── pages/ # generated/custom UI and route registrations\n├── bff/ # SWA managed HTTP-only Functions\n│ ├── src/ # BFF handlers and request-local helpers\n│ └── {function}/ # function.json registrations"
92
+ : "├── app/ # Next.js App Router\n│ ├── api/ # BFF routes\n│ └── {model}/ # generated/custom UI",
93
+ devGuidance: react
94
+ ? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately."
95
+ : "Use http://localhost:3000 for the app, or the SWA emulator port (4280) when testing SWA authentication. `swallowkit dev` runs Next.js and standalone Functions (7071). The BFF remains Next.js API routes.",
83
96
  pm: context.packageManager,
84
97
  runCmd,
85
98
  };