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
@@ -0,0 +1,292 @@
1
+ import { AuthConfig } from "../../types";
2
+ import { normalizeAuthConfig } from "../config";
3
+ import { ModelInfo, toCamelCase } from "./model-parser";
4
+
5
+ export function generateManagedFunctionConfig(
6
+ route: string,
7
+ methods: readonly string[],
8
+ entry: string
9
+ ): string {
10
+ return (
11
+ JSON.stringify(
12
+ {
13
+ scriptFile: `../dist/${entry}/index.js`,
14
+ bindings: [
15
+ {
16
+ authLevel: "anonymous",
17
+ type: "httpTrigger",
18
+ direction: "in",
19
+ name: "req",
20
+ methods: methods.map((method) => method.toLowerCase()),
21
+ route,
22
+ },
23
+ { type: "http", direction: "out", name: "$return" },
24
+ ],
25
+ },
26
+ null,
27
+ 2
28
+ ) + "\n"
29
+ );
30
+ }
31
+
32
+ export function getAuthCookieName(projectName: string): string {
33
+ return (
34
+ projectName.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") +
35
+ "-auth-token"
36
+ );
37
+ }
38
+
39
+ /** Request-local credentials: never share authentication state between invocations. */
40
+ export function generateReactBffHelper(
41
+ projectName: string,
42
+ auth?: AuthConfig
43
+ ): string {
44
+ const providers = new Set(
45
+ Object.values(normalizeAuthConfig(auth)?.schemes ?? {}).map(
46
+ (scheme) => scheme.provider
47
+ )
48
+ );
49
+ return `import type { HttpRequest, HttpResponse } from '@azure/functions';
50
+ import { z } from 'zod/v4';
51
+
52
+ // SwallowKit BFF Call Function Helper (SWA managed Functions)
53
+ const forwardSwa = ${providers.has("swa")};
54
+ const forwardBearer = ${providers.has("custom-jwt") || providers.has("external-token")};
55
+ const authCookieName = '${getAuthCookieName(projectName)}';
56
+
57
+ export class BffError extends Error {
58
+ constructor(public status: number, message: string) { super(message); }
59
+ }
60
+
61
+ export function json(body: unknown, status = 200): HttpResponse {
62
+ return { status, body, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } };
63
+ }
64
+
65
+ export function parseBody(request: HttpRequest): unknown {
66
+ if (request.body === undefined || request.body === '') throw new BffError(400, 'Request body is required');
67
+ if (typeof request.body !== 'string') return request.body;
68
+ try { return JSON.parse(request.body); }
69
+ catch { throw new BffError(400, 'Invalid JSON body'); }
70
+ }
71
+
72
+ function credentialHeaders(request: HttpRequest): Record<string, string> {
73
+ const result: Record<string, string> = {};
74
+ if (forwardBearer) {
75
+ const authorization = request.headers.authorization;
76
+ const cookie = request.headers.cookie?.split(';').map(value => value.trim()).find(value => value.startsWith(authCookieName + '='));
77
+ if (authorization?.startsWith('Bearer ')) result.Authorization = authorization;
78
+ else if (cookie) {
79
+ try { result.Authorization = 'Bearer ' + decodeURIComponent(cookie.slice(authCookieName.length + 1)); }
80
+ catch { throw new BffError(401, 'Invalid authentication cookie'); }
81
+ }
82
+ }
83
+ // SWA injects this header at the trusted managed API boundary. Never expose the backend key to clients.
84
+ if (forwardSwa && request.headers['x-ms-client-principal']) {
85
+ const encoded = request.headers['x-ms-client-principal'];
86
+ try {
87
+ const decoded = Buffer.from(encoded, 'base64');
88
+ if (decoded.toString('base64') !== encoded) throw new Error('Invalid encoding');
89
+ const value = JSON.parse(decoded.toString('utf8'));
90
+ if (typeof value.identityProvider !== 'string' || !value.identityProvider ||
91
+ typeof value.userId !== 'string' || !value.userId ||
92
+ !Array.isArray(value.userRoles) || !value.userRoles.includes('authenticated')) {
93
+ throw new Error('Invalid principal');
94
+ }
95
+ result['x-ms-client-principal'] = Buffer.from(JSON.stringify({
96
+ identityProvider: value.identityProvider, userId: value.userId,
97
+ userDetails: typeof value.userDetails === 'string' ? value.userDetails : '',
98
+ userRoles: value.userRoles.filter((role: unknown) => typeof role === 'string'),
99
+ })).toString('base64');
100
+ } catch {
101
+ throw new BffError(401, 'Invalid SWA client principal');
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ export async function backendRequest(request: HttpRequest, method: string, path: string, body?: unknown): Promise<Response> {
108
+ const baseUrl = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
109
+ const parsedUrl = new URL(baseUrl);
110
+ const local = parsedUrl.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsedUrl.hostname);
111
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
112
+ if (!key && !local) throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
113
+ if (!local && parsedUrl.protocol !== 'https:') throw new Error('Remote Functions calls require HTTPS');
114
+ return fetch(baseUrl.replace(/\\/$/, '') + path, {
115
+ method,
116
+ headers: { 'Content-Type': 'application/json', ...credentialHeaders(request), ...(key ? { 'x-functions-key': key } : {}) },
117
+ body: body === undefined ? undefined : JSON.stringify(body),
118
+ redirect: 'error',
119
+ });
120
+ }
121
+
122
+ interface CallFunctionConfig {
123
+ request: HttpRequest;
124
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
125
+ path: string;
126
+ body?: unknown;
127
+ inputSchema?: z.ZodType;
128
+ responseSchema?: z.ZodType;
129
+ successStatus?: number;
130
+ }
131
+
132
+ export async function callFunction(config: CallFunctionConfig): Promise<HttpResponse> {
133
+ try {
134
+ let body = config.body;
135
+ if (config.inputSchema) {
136
+ const result = config.inputSchema.safeParse(body);
137
+ if (!result.success) return json({ error: 'Validation failed', details: result.error.issues }, 400);
138
+ body = result.data;
139
+ }
140
+ const response = await backendRequest(config.request, config.method, config.path, body);
141
+ if (response.status === 204) return { status: 204 };
142
+ if (!response.ok) {
143
+ console.error('[BFF] Backend request failed:', response.status);
144
+ return json({ error: 'Backend request failed' }, response.status);
145
+ }
146
+ const data: unknown = await response.json();
147
+ if (config.responseSchema) {
148
+ const result = config.responseSchema.safeParse(data);
149
+ if (!result.success) {
150
+ console.error('[BFF] Backend response validation failed');
151
+ return json({ error: 'Invalid backend response' }, 502);
152
+ }
153
+ return json(result.data, config.successStatus ?? 200);
154
+ }
155
+ return json(data, config.successStatus ?? 200);
156
+ } catch (error) {
157
+ return handleError(error);
158
+ }
159
+ }
160
+
161
+ export function handleError(error: unknown): HttpResponse {
162
+ if (error instanceof BffError) return json({ error: error.message }, error.status);
163
+ console.error('[BFF] Request failed:', error instanceof Error ? error.name : 'UnknownError');
164
+ return json({ error: 'Backend request failed' }, 502);
165
+ }
166
+ `;
167
+ }
168
+
169
+ export function generateReactBffRoute(
170
+ model: ModelInfo,
171
+ sharedPackageName: string
172
+ ): string {
173
+ const modelCamel = toCamelCase(model.name);
174
+ const operations = new Set(
175
+ model.connectorConfig?.operations ?? [
176
+ "getAll",
177
+ "getById",
178
+ "create",
179
+ "update",
180
+ "delete",
181
+ ]
182
+ );
183
+ const managedFields = model.fields
184
+ .filter((field) => ["id", "createdAt", "updatedAt"].includes(field.name))
185
+ .map((field) => `${field.name}: true`)
186
+ .join(", ");
187
+ const clauses: string[] = [];
188
+ if (operations.has("getAll"))
189
+ clauses.push(
190
+ ` if (request.method === 'GET' && !id) return callFunction({ request, method: 'GET', path, responseSchema: z.array(${model.schemaName}) });`
191
+ );
192
+ if (operations.has("getById"))
193
+ clauses.push(
194
+ ` if (request.method === 'GET' && id) return callFunction({ request, method: 'GET', path, responseSchema: ${model.schemaName} });`
195
+ );
196
+ if (operations.has("create"))
197
+ clauses.push(
198
+ ` if (request.method === 'POST' && !id) return callFunction({ request, method: 'POST', path, body: parseBody(request), inputSchema: InputSchema, responseSchema: ${model.schemaName}, successStatus: 201 });`
199
+ );
200
+ if (operations.has("update"))
201
+ clauses.push(
202
+ ` if (request.method === 'PUT' && id) return callFunction({ request, method: 'PUT', path, body: parseBody(request), inputSchema: InputSchema, responseSchema: ${model.schemaName} });`
203
+ );
204
+ if (operations.has("delete"))
205
+ clauses.push(
206
+ ` if (request.method === 'DELETE' && id) return callFunction({ request, method: 'DELETE', path });`
207
+ );
208
+ return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
209
+ import { z } from 'zod/v4';
210
+ import { ${model.schemaName} } from '${sharedPackageName}';
211
+ import { callFunction, parseBody, json, handleError } from '../lib/call-function';
212
+
213
+ const InputSchema = ${model.schemaName}.omit({ ${managedFields} });
214
+
215
+ const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
216
+ try {
217
+ const id = request.params.id;
218
+ const path = '/api/${modelCamel}' + (id ? '/' + encodeURIComponent(id) : '');
219
+ ${clauses.join("\n")}
220
+ return json({ error: 'Method not allowed' }, 405);
221
+ } catch (error) { return handleError(error); }
222
+ };
223
+ export default handler;
224
+ `;
225
+ }
226
+
227
+ export function generateReactGreetBff(): string {
228
+ return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
229
+ import { callFunction, parseBody, handleError } from '../lib/call-function';
230
+
231
+ const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
232
+ try {
233
+ const body = request.method === 'POST' ? parseBody(request) : undefined;
234
+ return callFunction({
235
+ request, method: request.method === 'POST' ? 'POST' : 'GET',
236
+ path: '/api/greet?name=' + encodeURIComponent(request.query.name || 'World'), body,
237
+ });
238
+ } catch (error) { return handleError(error); }
239
+ };
240
+ export default handler;
241
+ `;
242
+ }
243
+
244
+ export function generateReactAuthBff(
245
+ projectName: string,
246
+ sharedPackageName: string,
247
+ customJwt: boolean
248
+ ): string {
249
+ return `import type { AzureFunction, Context, HttpRequest } from '@azure/functions';
250
+ ${customJwt ? `import { z } from 'zod/v4';\nimport { AuthUser, LoginRequest, LoginResponse } from '${sharedPackageName}';` : ""}
251
+ import { backendRequest, callFunction, parseBody, json, handleError } from '../lib/call-function';
252
+
253
+ const cookieName = '${getAuthCookieName(projectName)}';
254
+ ${
255
+ customJwt
256
+ ? `const CurrentUser = z.preprocess(value => {
257
+ if (!value || typeof value !== 'object') return value;
258
+ return { ...value, id: 'id' in value ? value.id : 'sub' in value ? value.sub : undefined };
259
+ }, AuthUser);
260
+ `
261
+ : ""
262
+ }
263
+ const handler: AzureFunction = async (_context: Context, request: HttpRequest) => {
264
+ try {
265
+ const action = request.params.action;
266
+ if (action === 'me' && request.method === 'GET') return callFunction({ request, method: 'GET', path: '/api/auth/me'${customJwt ? ", responseSchema: CurrentUser" : ""} });
267
+ ${
268
+ customJwt
269
+ ? ` if (action === 'logout' && request.method === 'POST') {
270
+ const response = json({ message: 'Logged out' });
271
+ response.headers = { ...response.headers, 'Set-Cookie': cookieName + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' };
272
+ return response;
273
+ }
274
+ if (action === 'login' && request.method === 'POST') {
275
+ const input = LoginRequest.safeParse(parseBody(request));
276
+ if (!input.success) return json({ error: 'Validation failed', details: input.error.issues }, 400);
277
+ const result = await backendRequest(request, 'POST', '/api/auth/login', input.data);
278
+ if (!result.ok) return json({ error: 'Authentication failed' }, result.status);
279
+ const data = LoginResponse.parse(await result.json());
280
+ const response = json({ user: data.user });
281
+ const secure = process.env.WEBSITE_SITE_NAME || process.env.NODE_ENV === 'production';
282
+ response.headers = { ...response.headers, 'Set-Cookie': cookieName + '=' + encodeURIComponent(data.token) + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400' + (secure ? '; Secure' : '') };
283
+ return response;
284
+ }`
285
+ : ""
286
+ }
287
+ return json({ error: 'Not found' }, 404);
288
+ } catch (error) { return handleError(error); }
289
+ };
290
+ export default handler;
291
+ `;
292
+ }
@@ -0,0 +1,51 @@
1
+ import { ModelInfo, toKebabCase } from "./model-parser";
2
+ import { UIAuthOptions } from "./ui-generator";
3
+
4
+ /** Adapt the shared CRUD presentation templates to React Router, without a Next.js runtime. */
5
+ export function adaptReactPage(source: string): string {
6
+ return source
7
+ .replace(/^'use client';?\s*\r?\n/, "")
8
+ .replace(
9
+ /import Link from 'next\/link';/g,
10
+ "import { Link } from 'react-router-dom';"
11
+ )
12
+ .replace(
13
+ /import \{ useParams, useRouter \} from 'next\/navigation';/g,
14
+ "import { useParams, useNavigate } from 'react-router-dom';"
15
+ )
16
+ .replace(
17
+ /import \{ useRouter \} from 'next\/navigation';/g,
18
+ "import { useNavigate } from 'react-router-dom';"
19
+ )
20
+ .replace(/\buseRouter\(\)/g, "useNavigate()")
21
+ .replace(/\brouter\.(?:push|replace)\(/g, "router(")
22
+ .replace(/^\s*router\.refresh\(\);?\s*$/gm, "")
23
+ .replace(/(<Link\b[^>]*?)\bhref=/gs, "$1to=")
24
+ .replace(/\[params\]/g, "[params.id]");
25
+ }
26
+
27
+ export function generateReactModelRoutes(
28
+ model: ModelInfo,
29
+ auth?: UIAuthOptions
30
+ ): string {
31
+ const slug = toKebabCase(model.name);
32
+ return `import type { RouteObject } from 'react-router-dom';
33
+ ${auth ? "import { Outlet } from 'react-router-dom';\nimport Layout from './layout';" : ""}
34
+ import List from './page';
35
+ import Detail from './[id]/page';
36
+ import New from './new/page';
37
+ import Edit from './[id]/edit/page';
38
+
39
+ const routes: RouteObject[] = [{
40
+ path: '/${slug}',
41
+ ${auth ? "element: <Layout><Outlet /></Layout>," : ""}
42
+ children: [
43
+ { index: true, element: <List /> },
44
+ { path: 'new', element: <New /> },
45
+ { path: ':id', element: <Detail /> },
46
+ { path: ':id/edit', element: <Edit /> },
47
+ ],
48
+ }];
49
+ export default routes;
50
+ `;
51
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SwallowKit - Next.js framework optimized for Azure deployment
2
+ * SwallowKit - React and Next.js application scaffolding for Azure
3
3
  * Main exports
4
4
  */
5
5
 
@@ -9,6 +9,7 @@ export type {
9
9
  UseServerFnResult,
10
10
  ServerFnMode,
11
11
  BackendLanguage,
12
+ FrontendFramework,
12
13
  SwallowKitConfig,
13
14
  } from "./types";
14
15
 
@@ -17,6 +18,7 @@ export {
17
18
  loadConfig,
18
19
  generateConfig,
19
20
  getBackendLanguage,
21
+ getFrontendFramework,
20
22
  getFullConfig,
21
23
  validateConfig,
22
24
  } from "./core/config";
@@ -272,7 +272,7 @@ function createMachineProgram(): Command {
272
272
  .description("Generate scaffold artifacts with deterministic JSON output")
273
273
  .argument("<model>", "Model file or model name")
274
274
  .option("--functions-dir <dir>", "Functions directory", "functions")
275
- .option("--api-dir <dir>", "API routes directory", "app/api")
275
+ .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
276
276
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
277
277
  .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
278
278
  await handleMachineAction("generate-scaffold", async () => {
@@ -292,7 +292,7 @@ function createMachineProgram(): Command {
292
292
  .description("Compute the scaffold change plan without writing any files (accepts multiple models)")
293
293
  .argument("<models...>", "Model files or model names")
294
294
  .option("--functions-dir <dir>", "Functions directory", "functions")
295
- .option("--api-dir <dir>", "API routes directory", "app/api")
295
+ .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
296
296
  .option("--api-only", "Skip UI components in the plan", false)
297
297
  .action(async (models: string[], options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
298
298
  await handleMachineAction(
@@ -6,6 +6,7 @@
6
6
  export type ServerFnMode = "auto" | "force-server" | "force-client";
7
7
 
8
8
  export type BackendLanguage = "typescript" | "csharp" | "python";
9
+ export type FrontendFramework = "nextjs" | "react";
9
10
 
10
11
  // useServerFn のオプション
11
12
  export interface UseServerFnOptions {
@@ -146,6 +147,9 @@ export interface AuthPrincipal {
146
147
 
147
148
  // CLI設定の型
148
149
  export interface SwallowKitConfig {
150
+ frontend?: {
151
+ framework?: FrontendFramework;
152
+ };
149
153
  database?: {
150
154
  connectionString?: string;
151
155
  databaseName?: string;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { execSync } from "child_process";
4
- import { BackendLanguage } from "../types";
4
+ import { BackendLanguage, FrontendFramework } from "../types";
5
5
 
6
6
  /**
7
7
  * Supported package managers
@@ -156,7 +156,7 @@ export function getWorkspaceConfig(pm: PackageManager, workspaces: string[]) {
156
156
  /** pnpm uses pnpm-workspace.yaml */
157
157
  type: "file" as const,
158
158
  filename: "pnpm-workspace.yaml",
159
- content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\nallowBuilds:\n protobufjs: false\n unrs-resolver: false\n`,
159
+ content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\nallowBuilds:\n keytar: false\n protobufjs: false\n unrs-resolver: false\n`,
160
160
  };
161
161
  }
162
162
 
@@ -214,7 +214,10 @@ export function getAzurePipelinesSetup(pm: PackageManager): string {
214
214
  /**
215
215
  * Build script for generated package.json (depends on workspace command syntax)
216
216
  */
217
- export function getBuildScript(pm: PackageManager): string {
217
+ export function getBuildScript(pm: PackageManager, frontend: FrontendFramework = "nextjs"): string {
218
+ if (frontend === "react") {
219
+ return `${getCommands(pm).runFilter("shared")} build && tsc --noEmit && vite build && node -e "require('fs').copyFileSync('staticwebapp.config.json','dist/staticwebapp.config.json')" && ${getCommands(pm).runFilter("bff")} build`;
220
+ }
218
221
  const copyStandaloneAssets = `node -e "const fs=require('fs');fs.mkdirSync('.next/standalone/.next',{recursive:true});if(fs.existsSync('.next/static'))fs.cpSync('.next/static','.next/standalone/.next/static',{recursive:true});if(fs.existsSync('public'))fs.cpSync('public','.next/standalone/public',{recursive:true});"`;
219
222
 
220
223
  if (pm === "pnpm") {