swallowkit 1.1.2 → 2.0.0-alpha.2

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 (88) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -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 +5 -2
  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__/package-manager.test.ts +15 -0
  68. package/src/__tests__/react.test.ts +819 -0
  69. package/src/__tests__/release-metadata.test.ts +121 -0
  70. package/src/cli/commands/add-auth.ts +67 -23
  71. package/src/cli/commands/dev.ts +109 -46
  72. package/src/cli/commands/init.ts +75 -35
  73. package/src/cli/commands/scaffold.ts +40 -19
  74. package/src/cli/index.ts +6 -3
  75. package/src/core/config.ts +21 -1
  76. package/src/core/project/agent-instructions.ts +14 -1
  77. package/src/core/project/agent-templates/AGENTS.md +24 -9
  78. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  79. package/src/core/project/capabilities.ts +14 -0
  80. package/src/core/project/manifest.ts +52 -27
  81. package/src/core/project/react-project.ts +507 -0
  82. package/src/core/project/validation.ts +32 -10
  83. package/src/core/scaffold/react-bff-generator.ts +292 -0
  84. package/src/core/scaffold/react-ui-generator.ts +51 -0
  85. package/src/index.ts +3 -1
  86. package/src/machine/index.ts +2 -2
  87. package/src/types/index.ts +4 -0
  88. package/src/utils/package-manager.ts +6 -3
@@ -393,9 +393,9 @@ This is a full-stack application deployed on Azure with a TypeScript
393
393
  frontend/BFF and an Azure Functions backend in {{backendLanguageLabel}}.
394
394
 
395
395
  ```text
396
- Frontend (React / Next.js App Router)
396
+ Frontend ({{frontendLabel}})
397
397
  ↓
398
- BFF (Next.js API Routes)
398
+ BFF ({{bffLabel}})
399
399
  ↓
400
400
  Azure Functions
401
401
  ↓
@@ -404,7 +404,7 @@ Azure Cosmos DB
404
404
 
405
405
  ### Core architecture rules
406
406
 
407
- 1. Next.js API routes are a BFF/proxy layer. Do not place application business
407
+ 1. {{bffLabel}} are a BFF/proxy layer. Do not place application business
408
408
  logic or direct database access there.
409
409
  2. Zod schemas under `shared/models/` are the application model source of truth.
410
410
  3. Do not duplicate model definitions across layers.
@@ -415,6 +415,11 @@ Azure Cosmos DB
415
415
  markers and boundaries.
416
416
  7. Production access to Azure resources must follow the generated security
417
417
  architecture; do not introduce hard-coded credentials or connection strings.
418
+ 8. Never link standalone Functions to SWA. The same-origin BFF calls the
419
+ independent backend with server-side BACKEND_FUNCTIONS_BASE_URL and
420
+ BACKEND_FUNCTIONS_KEY. Never expose these values in browser bundles or VITE_*.
421
+ Managed Functions use HTTP triggers only; database access, non-HTTP triggers,
422
+ managed identity, and private networking belong to the standalone backend.
418
423
 
419
424
  ### Backend-specific rules
420
425
 
@@ -427,16 +432,13 @@ values for these fields.
427
432
 
428
433
  ```text
429
434
  {{projectName}}/
430
- ├── app/ # Next.js App Router
431
- │ ├── api/ # BFF routes
432
- │ └── {model}/ # generated/custom UI
435
+ {{frontendStructure}}
433
436
  ├── functions/ # Azure Functions backend
434
437
  {{functionsStructureLine}}
435
438
  ├── shared/
436
439
  │ ├── models/ # Zod source-of-truth models
437
440
  │ └── index.ts
438
- ├── lib/
439
- │ └── api/
441
+ ├── lib/ # auth, navigation, and shared application utilities
440
442
  ├── components/
441
443
  ├── infra/ # Bicep infrastructure
442
444
  ├── .swallowkit/ # SwallowKit project metadata and runbooks
@@ -470,13 +472,26 @@ same generated structure.
470
472
  {{pm}} install
471
473
  {{runCmd}} swallowkit dev
472
474
  {{pm}} run build
473
- {{pm}} run lint
475
+ {{pm}} run --if-present lint
474
476
  {{runCmd}} swallowkit machine verify project
475
477
  ```
476
478
 
477
479
  Always invoke SwallowKit using the package-runner convention generated for this
478
480
  project. Do not arbitrarily mix package-manager invocation styles.
479
481
 
482
+ {{devGuidance}}
483
+
484
+ Check for an already-running Cosmos DB Emulator before database tests. If it is
485
+ unavailable, report that prerequisite; do not start/restart it or change global
486
+ tools, TLS trust, or machine settings without explicit permission. Test against
487
+ an isolated database and ports, never reset user data, and stop only processes
488
+ you started. `dev --seed-env` replaces data, so inspect its target first.
489
+
490
+ Build and run `verify project`, then exercise UI, CRUD, error/validation paths,
491
+ and configured authentication through the same-origin BFF. A passing typecheck
492
+ or server readiness probe alone is not an end-to-end verification. Report
493
+ unverified cloud deployment or unavailable local prerequisites explicitly.
494
+
480
495
  ## Final Rules
481
496
 
482
497
  Do not:
@@ -1,5 +1,5 @@
1
1
  ---
2
- applyTo: "app/api/**"
2
+ applyTo: "{{bffGlob}}"
3
3
  ---
4
4
 
5
5
  # BFF API Routes — Rules
@@ -7,10 +7,12 @@ applyTo: "app/api/**"
7
7
  The root `AGENTS.md` is the canonical contract. Inspect the current
8
8
  responsibility boundary before editing files in this layer.
9
9
 
10
- - `app/api/` is a BFF/proxy layer. Never place business logic, database access,
10
+ - `{{bffPath}}/` is a BFF/proxy layer. Never place business logic, database access,
11
11
  or direct Cosmos DB calls here.
12
- - Use `callFunction()` from `@/lib/api/call-function` to forward requests to
12
+ - Use `callFunction()` from `{{helperImport}}` to forward requests to
13
13
  Azure Functions.
14
+ - Keep credentials request-local. Never put the backend URL/key in client
15
+ code or VITE_* variables. Do not link standalone Functions to SWA.
14
16
  - Input/output validation may use schemas imported from `@{{projectName}}/shared`.
15
17
  - Never directly edit a route reported as deterministic. Change its source of
16
18
  truth and use MCP-first plan/apply, with the machine interface as fallback.
@@ -27,6 +27,13 @@ export interface CapabilityAuthProvider {
27
27
  export interface CapabilitiesContract {
28
28
  contractVersion: number;
29
29
  swallowkitVersion: string;
30
+ frontends: {
31
+ supported: string[];
32
+ default: string;
33
+ configuration: string;
34
+ reactArchitecture: string;
35
+ development: string;
36
+ };
30
37
  modelDeclarations: CapabilityModelDeclaration[];
31
38
  authentication: {
32
39
  correctWorkflow: string[];
@@ -64,6 +71,13 @@ export function inspectCapabilities(): CapabilitiesContract {
64
71
  return {
65
72
  contractVersion: 1,
66
73
  swallowkitVersion: getSwallowKitVersion(),
74
+ frontends: {
75
+ supported: ["nextjs", "react"],
76
+ default: "nextjs",
77
+ configuration: "init --frontend nextjs|react; stored as frontend.framework in swallowkit.config.js. Missing settings preserve Next.js.",
78
+ reactArchitecture: "React/Vite SPA on SWA -> HTTP-only SWA managed Functions BFF (bff/) -> standalone Functions (functions/). Never link standalone Functions to SWA. Backend keys stay server-side.",
79
+ development: "React dev runs Vite:3000, BFF:7073, Functions:7071 and SWA CLI:4280; use the SWA port for same-origin APIs and authentication. Build/verify include BFF typechecking.",
80
+ },
67
81
  modelDeclarations: [
68
82
  {
69
83
  name: "schema",
@@ -1,8 +1,8 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
3
+ import { getBackendLanguage, getFrontendFramework, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
4
4
  import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
5
- import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
5
+ import { AuthConfig, BackendLanguage, FrontendFramework, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
6
6
  import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
7
7
 
8
8
  const CONFIG_CANDIDATES = [
@@ -69,6 +69,7 @@ export interface ProjectManifestArtifacts {
69
69
 
70
70
  export interface ProjectManifestArchitecture {
71
71
  pattern: string;
72
+ frontendFramework?: FrontendFramework;
72
73
  backendLanguage: BackendLanguage;
73
74
  layerBoundaries: string[];
74
75
  hasSharedWorkspace: boolean;
@@ -81,6 +82,7 @@ export interface ProjectManifest {
81
82
  generatedAt: string;
82
83
  configPath: string | null;
83
84
  backendLanguage: BackendLanguage;
85
+ frontendFramework?: FrontendFramework;
84
86
  configValidation: {
85
87
  valid: boolean;
86
88
  errors: string[];
@@ -115,7 +117,10 @@ function relativeProjectPath(projectRoot: string, targetPath: string): string {
115
117
  return path.relative(projectRoot, targetPath).replace(/\\/g, "/");
116
118
  }
117
119
 
118
- function buildModules(projectRoot: string): ProjectManifestModule[] {
120
+ function buildModules(projectRoot: string, frontend: FrontendFramework): ProjectManifestModule[] {
121
+ const bffRoot = frontend === "react" ? "bff" : "app/api";
122
+ const uiRoot = frontend === "react" ? "src" : "app";
123
+ const authRoot = frontend === "react" ? "bff/src/auth" : "app/api/auth";
119
124
  const candidates: ProjectManifestModule[] = [
120
125
  {
121
126
  name: "shared-models",
@@ -126,8 +131,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
126
131
  {
127
132
  name: "bff",
128
133
  kind: "bff",
129
- rootPath: "app/api",
130
- exists: fs.existsSync(path.join(projectRoot, "app", "api")),
134
+ rootPath: bffRoot,
135
+ exists: fs.existsSync(path.join(projectRoot, bffRoot)),
131
136
  },
132
137
  {
133
138
  name: "functions",
@@ -138,8 +143,8 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
138
143
  {
139
144
  name: "ui",
140
145
  kind: "ui",
141
- rootPath: "app",
142
- exists: fs.existsSync(path.join(projectRoot, "app")),
146
+ rootPath: uiRoot,
147
+ exists: fs.existsSync(path.join(projectRoot, uiRoot)),
143
148
  },
144
149
  {
145
150
  name: "infrastructure",
@@ -150,21 +155,21 @@ function buildModules(projectRoot: string): ProjectManifestModule[] {
150
155
  {
151
156
  name: "auth",
152
157
  kind: "auth",
153
- rootPath: "app/api/auth",
154
- exists: fs.existsSync(path.join(projectRoot, "app", "api", "auth")),
158
+ rootPath: authRoot,
159
+ exists: fs.existsSync(path.join(projectRoot, authRoot)),
155
160
  },
156
161
  ];
157
162
 
158
163
  return candidates;
159
164
  }
160
165
 
161
- function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
166
+ function buildArtifacts(projectRoot: string, frontend: FrontendFramework): ProjectManifestArtifacts {
162
167
  const openApiDir = path.join(projectRoot, "functions", "openapi");
163
168
  const generatedDir = path.join(projectRoot, "functions", "generated");
164
169
 
165
170
  return {
166
- callFunctionHelperPath: "lib/api/call-function.ts",
167
- callFunctionHelperExists: fs.existsSync(path.join(projectRoot, "lib", "api", "call-function.ts")),
171
+ callFunctionHelperPath: frontend === "react" ? "bff/src/lib/call-function.ts" : "lib/api/call-function.ts",
172
+ callFunctionHelperExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["bff", "src", "lib", "call-function.ts"] : ["lib", "api", "call-function.ts"]))),
168
173
  openApiSpecs: fs.existsSync(openApiDir)
169
174
  ? fs.readdirSync(openApiDir)
170
175
  .filter((entry) => entry.endsWith(".json"))
@@ -178,8 +183,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
178
183
  : [],
179
184
  proxyPath: "proxy.ts",
180
185
  proxyExists: fs.existsSync(path.join(projectRoot, "proxy.ts")),
181
- loginPagePath: "app/login/page.tsx",
182
- loginPageExists: fs.existsSync(path.join(projectRoot, "app", "login", "page.tsx")),
186
+ loginPagePath: frontend === "react" ? "src/pages/login.tsx" : "app/login/page.tsx",
187
+ loginPageExists: fs.existsSync(path.join(projectRoot, ...(frontend === "react" ? ["src", "pages", "login.tsx"] : ["app", "login", "page.tsx"]))),
183
188
  authContextPath: "lib/auth/auth-context.tsx",
184
189
  authContextExists: fs.existsSync(path.join(projectRoot, "lib", "auth", "auth-context.tsx")),
185
190
  };
@@ -188,7 +193,8 @@ function buildArtifacts(projectRoot: string): ProjectManifestArtifacts {
188
193
  function buildEntityRoutes(
189
194
  projectRoot: string,
190
195
  backendLanguage: BackendLanguage,
191
- entity: ProjectManifestEntity
196
+ entity: ProjectManifestEntity,
197
+ frontend: FrontendFramework
192
198
  ): ProjectManifestRoute[] {
193
199
  const modelCamel = toCamelCase(entity.name);
194
200
  const modelKebab = toKebabCase(entity.name);
@@ -229,8 +235,10 @@ function buildEntityRoutes(
229
235
  entityName: entity.name,
230
236
  methods: listMethods,
231
237
  publicPath: `/api/${modelCamel}`,
232
- filePath: `app/api/${modelCamel}/route.ts`,
233
- exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
238
+ filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/route.ts`,
239
+ exists: frontend === "react"
240
+ ? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
241
+ : fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "route.ts")),
234
242
  },
235
243
  {
236
244
  name: `${entity.name}-bff-detail`,
@@ -239,8 +247,10 @@ function buildEntityRoutes(
239
247
  entityName: entity.name,
240
248
  methods: detailMethods,
241
249
  publicPath: `/api/${modelCamel}/{id}`,
242
- filePath: `app/api/${modelCamel}/[id]/route.ts`,
243
- exists: fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
250
+ filePath: frontend === "react" ? `bff/src/${modelCamel}/index.ts` : `app/api/${modelCamel}/[id]/route.ts`,
251
+ exists: frontend === "react"
252
+ ? fs.existsSync(path.join(projectRoot, "bff", "src", modelCamel, "index.ts")) && fs.existsSync(path.join(projectRoot, "bff", modelCamel, "function.json"))
253
+ : fs.existsSync(path.join(projectRoot, "app", "api", modelCamel, "[id]", "route.ts")),
244
254
  },
245
255
  {
246
256
  name: `${entity.name}-functions`,
@@ -255,7 +265,18 @@ function buildEntityRoutes(
255
265
  ];
256
266
  }
257
267
 
258
- function buildSystemRoutes(projectRoot: string): ProjectManifestRoute[] {
268
+ function buildSystemRoutes(projectRoot: string, frontend: FrontendFramework, auth?: AuthConfig): ProjectManifestRoute[] {
269
+ if (frontend === "react") {
270
+ const providers = Object.values(normalizeAuthConfig(auth)?.schemes ?? {}).map(scheme => scheme.provider);
271
+ return ["login", "logout", "me"].map(action => ({
272
+ name: `auth-${action}`, kind: "system", surface: "bff",
273
+ methods: [action === "me" ? "GET" : "POST"], publicPath: `/api/auth/${action}`,
274
+ filePath: "bff/src/auth/index.ts",
275
+ exists: (providers.includes("custom-jwt") || (action === "me" && providers.includes("external-token"))) &&
276
+ fs.existsSync(path.join(projectRoot, "bff", "src", "auth", "index.ts")) &&
277
+ fs.existsSync(path.join(projectRoot, "bff", "auth", "function.json")),
278
+ }));
279
+ }
259
280
  return [
260
281
  {
261
282
  name: "auth-login",
@@ -296,6 +317,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
296
317
  return withWorkingDirectory(projectRoot, async () => {
297
318
  const configPath = resolveConfigPath(projectRoot);
298
319
  const backendLanguage = getBackendLanguage(configPath || undefined);
320
+ const frontendFramework = getFrontendFramework(configPath || undefined);
299
321
  const config = getFullConfig(configPath || undefined);
300
322
  const configValidation = validateConfig(config);
301
323
  const { result: entities, messages } = await captureConsoleMessages(async () => {
@@ -327,8 +349,8 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
327
349
  );
328
350
 
329
351
  const routes = [
330
- ...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity)),
331
- ...buildSystemRoutes(projectRoot),
352
+ ...entities.flatMap((entity) => buildEntityRoutes(projectRoot, backendLanguage, entity, frontendFramework)),
353
+ ...buildSystemRoutes(projectRoot, frontendFramework, config.auth),
332
354
  ].sort((left, right) => left.name.localeCompare(right.name));
333
355
 
334
356
  const manifest: ProjectManifest = {
@@ -336,6 +358,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
336
358
  generatedAt: new Date().toISOString(),
337
359
  configPath,
338
360
  backendLanguage,
361
+ frontendFramework,
339
362
  configValidation,
340
363
  connectors: Object.entries(config.connectors || {})
341
364
  .map(([name, definition]) => ({ name, definition }))
@@ -343,16 +366,18 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
343
366
  auth: normalizeAuthConfig(config.auth) || null,
344
367
  entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
345
368
  routes,
346
- modules: buildModules(projectRoot),
347
- artifacts: buildArtifacts(projectRoot),
369
+ modules: buildModules(projectRoot, frontendFramework),
370
+ artifacts: buildArtifacts(projectRoot, frontendFramework),
348
371
  architecture: {
349
- pattern: "Next.js BFF + Azure Functions + shared Zod models",
372
+ pattern: frontendFramework === "react" ? "React SPA + SWA managed Functions BFF + standalone Azure Functions + shared Zod models" : "Next.js BFF + Azure Functions + shared Zod models",
373
+ frontendFramework,
350
374
  backendLanguage,
351
375
  layerBoundaries: [
352
376
  "shared/models -> schema source of truth",
353
- "app/api -> BFF layer only",
377
+ `${frontendFramework === "react" ? "bff" : "app/api"} -> BFF layer only`,
354
378
  "functions -> backend logic and data access",
355
- "app -> UI and pages",
379
+ `${frontendFramework === "react" ? "src" : "app"} -> UI and pages`,
380
+ "SWA and standalone Functions are not linked; backend credentials stay in the BFF",
356
381
  ],
357
382
  hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
358
383
  hasConnectors: Object.keys(config.connectors || {}).length > 0,