swallowkit 1.0.0-beta.46 → 1.0.0-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/add-auth.d.ts +2 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +214 -16
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +100 -12
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +56 -30
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +37 -5
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +20 -2
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +265 -12
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +19 -19
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -2
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +16 -16
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +13 -1
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
- package/src/__tests__/auth.test.ts +103 -0
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +19 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +239 -16
- package/src/cli/commands/dev.ts +111 -14
- package/src/cli/commands/init.ts +56 -30
- package/src/cli/commands/scaffold.ts +37 -5
- package/src/cli/index.ts +3 -1
- package/src/core/scaffold/auth-generator.ts +267 -13
- package/src/core/scaffold/connector-functions-generator.ts +22 -19
- package/src/core/scaffold/functions-generator.ts +17 -17
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/types/index.ts +1 -1
package/src/cli/commands/dev.ts
CHANGED
|
@@ -38,10 +38,13 @@ export interface DevOptions {
|
|
|
38
38
|
noFunctions?: boolean;
|
|
39
39
|
seedEnv?: string;
|
|
40
40
|
mockConnectors?: boolean;
|
|
41
|
+
swaPort?: string;
|
|
42
|
+
noSwa?: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
type ParsedDevActionOptions = DevOptions & {
|
|
44
46
|
functions?: boolean;
|
|
47
|
+
swa?: boolean;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
interface FunctionsCoreToolsCommand {
|
|
@@ -57,6 +60,7 @@ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions
|
|
|
57
60
|
return {
|
|
58
61
|
...options,
|
|
59
62
|
noFunctions: options.noFunctions ?? options.functions === false,
|
|
63
|
+
noSwa: options.noSwa ?? options.swa === false,
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -121,6 +125,23 @@ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: s
|
|
|
121
125
|
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
|
|
129
|
+
return [
|
|
130
|
+
'start',
|
|
131
|
+
`http://${host || 'localhost'}:${nextPort}`,
|
|
132
|
+
'--swa-config-location',
|
|
133
|
+
'.',
|
|
134
|
+
'--port',
|
|
135
|
+
swaPort,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getSwaCliInstallCommand(pm: string): string {
|
|
140
|
+
return pm === 'pnpm'
|
|
141
|
+
? 'pnpm add -Dw @azure/static-web-apps-cli'
|
|
142
|
+
: 'npm install -D @azure/static-web-apps-cli';
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
|
|
125
146
|
return backendLanguage === 'csharp' ? 90_000 : 30_000;
|
|
126
147
|
}
|
|
@@ -506,6 +527,8 @@ export function buildDevCommand(
|
|
|
506
527
|
.option('--no-functions', 'Skip Azure Functions startup')
|
|
507
528
|
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
508
529
|
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
530
|
+
.option('--swa-port <port>', 'SWA authentication emulator port', '4280')
|
|
531
|
+
.option('--no-swa', 'Skip the SWA authentication emulator')
|
|
509
532
|
.action(async (options: ParsedDevActionOptions) => {
|
|
510
533
|
const normalizedOptions = normalizeParsedDevOptions(options);
|
|
511
534
|
|
|
@@ -618,11 +641,24 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
|
|
|
618
641
|
async function startDevEnvironment(options: DevOptions) {
|
|
619
642
|
const port = options.port || '3000';
|
|
620
643
|
const functionsPort = options.functionsPort || '7071';
|
|
644
|
+
const swaPort = options.swaPort || '4280';
|
|
621
645
|
|
|
622
646
|
// Detect package manager from project lockfile
|
|
623
647
|
const pm = detectFromProject();
|
|
624
648
|
const pmCmd = getCommands(pm);
|
|
625
649
|
const backendLanguage = getBackendLanguage();
|
|
650
|
+
const authConfig = getAuthConfig();
|
|
651
|
+
const useSwaEmulator = authConfig?.provider === 'swa' && !options.noSwa;
|
|
652
|
+
const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
|
|
653
|
+
|
|
654
|
+
if (useSwaEmulator && !swaCommand) {
|
|
655
|
+
const installCommand = getSwaCliInstallCommand(pm);
|
|
656
|
+
console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
|
|
657
|
+
console.error(' Install it in this project and retry:');
|
|
658
|
+
console.error(`\n ${installCommand}\n`);
|
|
659
|
+
process.exitCode = 1;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
626
662
|
|
|
627
663
|
// プロセスを管理する配列
|
|
628
664
|
const processes: ChildProcess[] = [];
|
|
@@ -925,7 +961,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
925
961
|
const connectorModels = allModels.filter((m) => m.connectorConfig);
|
|
926
962
|
|
|
927
963
|
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
928
|
-
const authConfig = getAuthConfig();
|
|
929
964
|
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
930
965
|
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
931
966
|
// Read JWT_SECRET from functions/local.settings.json if available
|
|
@@ -985,18 +1020,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
985
1020
|
// 5. Start Next.js development server
|
|
986
1021
|
const nextArgs = buildNextDevArgs(pm, port);
|
|
987
1022
|
|
|
988
|
-
if (options.open) {
|
|
989
|
-
// Next.js 14+ deprecated --open option, so we open browser manually
|
|
990
|
-
setTimeout(() => {
|
|
991
|
-
const url = `http://${options.host || 'localhost'}:${port}`;
|
|
992
|
-
console.log(`\n🌐 Opening browser: ${url}`);
|
|
993
|
-
|
|
994
|
-
const start = process.platform === 'darwin' ? 'open' :
|
|
995
|
-
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
996
|
-
spawn(start, [url], { shell: true });
|
|
997
|
-
}, 3000);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1023
|
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1001
1024
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
1002
1025
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
@@ -1063,10 +1086,52 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1063
1086
|
}
|
|
1064
1087
|
}
|
|
1065
1088
|
|
|
1089
|
+
let swaReady = false;
|
|
1090
|
+
if (useSwaEmulator && swaCommand) {
|
|
1091
|
+
const nextUrl = `http://${options.host || 'localhost'}:${port}`;
|
|
1092
|
+
console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
|
|
1093
|
+
const nextReady = await waitForHttpServerReady(nextUrl);
|
|
1094
|
+
if (!nextReady) {
|
|
1095
|
+
throw new Error(`Next.js did not become ready at ${nextUrl}`);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
|
|
1099
|
+
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
|
|
1100
|
+
cwd: process.cwd(),
|
|
1101
|
+
shell: true,
|
|
1102
|
+
stdio: 'inherit',
|
|
1103
|
+
env: process.env,
|
|
1104
|
+
});
|
|
1105
|
+
processes.push(swaProcess);
|
|
1106
|
+
swaProcess.on('error', (error) => {
|
|
1107
|
+
console.error('❌ SWA CLI startup error:', error.message);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1111
|
+
swaReady = await waitForHttpServerReady(swaUrl);
|
|
1112
|
+
if (!swaReady) {
|
|
1113
|
+
throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1066
1117
|
console.log('');
|
|
1067
1118
|
console.log('✅ SwallowKit development environment is running!');
|
|
1068
1119
|
console.log('');
|
|
1069
|
-
|
|
1120
|
+
if (useSwaEmulator) {
|
|
1121
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1122
|
+
console.log('🔐 SWA authenticated app:');
|
|
1123
|
+
console.log(` ${swaUrl}`);
|
|
1124
|
+
console.log('');
|
|
1125
|
+
console.log('🔑 Local sign-in:');
|
|
1126
|
+
console.log(` ${swaUrl}/.auth/login/aad`);
|
|
1127
|
+
console.log(' Enter any username and select Login.');
|
|
1128
|
+
console.log(' The "authenticated" role is added automatically.');
|
|
1129
|
+
console.log('');
|
|
1130
|
+
console.log('📱 Next.js direct access (authentication is not emulated):');
|
|
1131
|
+
console.log(` http://${options.host || 'localhost'}:${port}`);
|
|
1132
|
+
} else {
|
|
1133
|
+
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
1134
|
+
}
|
|
1070
1135
|
if (hasFunctions && !options.noFunctions) {
|
|
1071
1136
|
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
1072
1137
|
}
|
|
@@ -1082,7 +1147,22 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1082
1147
|
if (mockServer) {
|
|
1083
1148
|
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
1084
1149
|
}
|
|
1150
|
+
if (swaReady) {
|
|
1151
|
+
console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
|
|
1152
|
+
}
|
|
1085
1153
|
console.log('');
|
|
1154
|
+
|
|
1155
|
+
if (options.open) {
|
|
1156
|
+
const url = useSwaEmulator
|
|
1157
|
+
? `http://${options.host || 'localhost'}:${swaPort}`
|
|
1158
|
+
: `http://${options.host || 'localhost'}:${port}`;
|
|
1159
|
+
console.log(`🌐 Opening browser: ${url}`);
|
|
1160
|
+
const start = process.platform === 'darwin' ? 'open' :
|
|
1161
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1162
|
+
spawn(start, [url], { shell: true });
|
|
1163
|
+
console.log('');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1086
1166
|
console.log('🛑 Press Ctrl+C to stop');
|
|
1087
1167
|
console.log('');
|
|
1088
1168
|
|
|
@@ -1097,6 +1177,23 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1097
1177
|
}
|
|
1098
1178
|
}
|
|
1099
1179
|
|
|
1180
|
+
async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
|
|
1181
|
+
const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
|
|
1182
|
+
const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
|
|
1183
|
+
if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
|
|
1184
|
+
cwd: projectRoot,
|
|
1185
|
+
shell: process.platform === 'win32',
|
|
1186
|
+
})) {
|
|
1187
|
+
return localCommand;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
|
|
1191
|
+
return 'swa';
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1100
1197
|
async function runCommand(
|
|
1101
1198
|
command: string,
|
|
1102
1199
|
args: string[],
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -730,6 +730,7 @@ export function buildGeneratedProjectDependencies(projectName: string): Record<s
|
|
|
730
730
|
return {
|
|
731
731
|
'@azure/cosmos': '^4.0.0',
|
|
732
732
|
'applicationinsights': '^3.3.0',
|
|
733
|
+
'zod': '^4.0.0',
|
|
733
734
|
[`@${projectName}/shared`]: 'workspace:*',
|
|
734
735
|
};
|
|
735
736
|
}
|
|
@@ -892,6 +893,13 @@ function getFunctionsBaseUrl(): string {
|
|
|
892
893
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
893
894
|
}
|
|
894
895
|
|
|
896
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
897
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
898
|
+
if (key) return { 'x-functions-key': key };
|
|
899
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
900
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
901
|
+
}
|
|
902
|
+
|
|
895
903
|
/**
|
|
896
904
|
* Simple HTTP client for calling backend APIs
|
|
897
905
|
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
@@ -903,6 +911,7 @@ async function request<T>(
|
|
|
903
911
|
queryParams?: Record<string, string>
|
|
904
912
|
): Promise<T> {
|
|
905
913
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
914
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
906
915
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
907
916
|
if (queryParams) {
|
|
908
917
|
const params = new URLSearchParams(queryParams);
|
|
@@ -914,6 +923,7 @@ async function request<T>(
|
|
|
914
923
|
method,
|
|
915
924
|
headers: {
|
|
916
925
|
'Content-Type': 'application/json',
|
|
926
|
+
...keyHeaders,
|
|
917
927
|
},
|
|
918
928
|
body: body ? JSON.stringify(body) : undefined,
|
|
919
929
|
});
|
|
@@ -997,6 +1007,7 @@ export const api = {
|
|
|
997
1007
|
// 6. Create .env.example
|
|
998
1008
|
const envExample = `# Azure Functions Backend URL
|
|
999
1009
|
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
1010
|
+
BACKEND_FUNCTIONS_KEY=
|
|
1000
1011
|
|
|
1001
1012
|
# Azure Configuration
|
|
1002
1013
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -1082,6 +1093,7 @@ export async function register() {
|
|
|
1082
1093
|
const envLocalContent = [
|
|
1083
1094
|
'# Azure Functions Backend URL (Local)',
|
|
1084
1095
|
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
1096
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
1085
1097
|
''
|
|
1086
1098
|
].join('\n');
|
|
1087
1099
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -1378,7 +1390,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
1378
1390
|
|
|
1379
1391
|
app.http('greet', {
|
|
1380
1392
|
methods: ['GET', 'POST'],
|
|
1381
|
-
authLevel: '
|
|
1393
|
+
authLevel: 'function',
|
|
1382
1394
|
handler: greet
|
|
1383
1395
|
});
|
|
1384
1396
|
`);
|
|
@@ -1410,7 +1422,7 @@ public sealed class GreetFunction
|
|
|
1410
1422
|
{
|
|
1411
1423
|
[Function("greet")]
|
|
1412
1424
|
public async Task<HttpResponseData> Run(
|
|
1413
|
-
[HttpTrigger(AuthorizationLevel.
|
|
1425
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1414
1426
|
{
|
|
1415
1427
|
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1416
1428
|
var name = "SwallowKit";
|
|
@@ -1478,7 +1490,7 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
1478
1490
|
|
|
1479
1491
|
from blueprints.greet import bp as greet_bp
|
|
1480
1492
|
|
|
1481
|
-
app = func.FunctionApp(http_auth_level=func.AuthLevel.
|
|
1493
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1482
1494
|
|
|
1483
1495
|
app.register_blueprint(greet_bp)
|
|
1484
1496
|
# SwallowKit scaffold registrations
|
|
@@ -2109,7 +2121,7 @@ const containerName = 'Models'; // PascalCase + 's'
|
|
|
2109
2121
|
app.http('{model}-get-all', {
|
|
2110
2122
|
methods: ['GET'],
|
|
2111
2123
|
route: '{model}',
|
|
2112
|
-
authLevel: '
|
|
2124
|
+
authLevel: 'function',
|
|
2113
2125
|
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
2114
2126
|
handler: async (request, context) => {
|
|
2115
2127
|
const documents = context.extraInputs.get('cosmosInput');
|
|
@@ -2400,7 +2412,7 @@ import { ModelSchema } from '@${projectName}/shared';
|
|
|
2400
2412
|
app.http('{model}-get-all', {
|
|
2401
2413
|
methods: ['GET'],
|
|
2402
2414
|
route: '{model}',
|
|
2403
|
-
authLevel: '
|
|
2415
|
+
authLevel: 'function',
|
|
2404
2416
|
extraInputs: [cosmosInput],
|
|
2405
2417
|
handler: async (request, context) => {
|
|
2406
2418
|
const documents = context.extraInputs.get(cosmosInput);
|
|
@@ -2431,6 +2443,43 @@ app.http('{model}-get-all', {
|
|
|
2431
2443
|
console.log('');
|
|
2432
2444
|
}
|
|
2433
2445
|
|
|
2446
|
+
export function buildFunctionsHostKeyBicepExpression(): string {
|
|
2447
|
+
return "listKeys('${functionsFlex.outputs.id}/host/default', '2023-12-01').functionKeys.default";
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
export function buildStaticWebAppConfigBicepSource(): string {
|
|
2451
|
+
return `@description('Static Web App name')
|
|
2452
|
+
param staticWebAppName string
|
|
2453
|
+
|
|
2454
|
+
@description('Functions App default hostname for backend API calls')
|
|
2455
|
+
param functionsDefaultHostname string
|
|
2456
|
+
|
|
2457
|
+
@secure()
|
|
2458
|
+
@description('Functions host key used only by the server-side BFF')
|
|
2459
|
+
param functionsHostKey string
|
|
2460
|
+
|
|
2461
|
+
@description('Application Insights connection string for SWA')
|
|
2462
|
+
param appInsightsConnectionString string
|
|
2463
|
+
|
|
2464
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2465
|
+
name: staticWebAppName
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2469
|
+
parent: staticWebApp
|
|
2470
|
+
name: 'appsettings'
|
|
2471
|
+
properties: {
|
|
2472
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2473
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2474
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2475
|
+
BACKEND_FUNCTIONS_KEY: functionsHostKey
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
output configName string = staticWebAppConfig.name
|
|
2480
|
+
`;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2434
2483
|
async function createInfrastructure(
|
|
2435
2484
|
projectDir: string,
|
|
2436
2485
|
projectName: string,
|
|
@@ -2605,6 +2654,7 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
2605
2654
|
params: {
|
|
2606
2655
|
staticWebAppName: staticWebApp.outputs.name
|
|
2607
2656
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2657
|
+
functionsHostKey: ${buildFunctionsHostKeyBicepExpression()}
|
|
2608
2658
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2609
2659
|
}
|
|
2610
2660
|
dependsOn: [
|
|
@@ -2698,31 +2748,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
2698
2748
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2699
2749
|
|
|
2700
2750
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2701
|
-
const staticWebAppConfigBicep =
|
|
2702
|
-
param staticWebAppName string
|
|
2703
|
-
|
|
2704
|
-
@description('Functions App default hostname for backend API calls')
|
|
2705
|
-
param functionsDefaultHostname string
|
|
2706
|
-
|
|
2707
|
-
@description('Application Insights connection string for SWA')
|
|
2708
|
-
param appInsightsConnectionString string
|
|
2709
|
-
|
|
2710
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2711
|
-
name: staticWebAppName
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2715
|
-
parent: staticWebApp
|
|
2716
|
-
name: 'appsettings'
|
|
2717
|
-
properties: {
|
|
2718
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2719
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2720
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
output configName string = staticWebAppConfig.name
|
|
2725
|
-
`;
|
|
2751
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2726
2752
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2727
2753
|
|
|
2728
2754
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
@@ -162,6 +162,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
162
162
|
? { authPolicy: uiAuthPolicy }
|
|
163
163
|
: undefined;
|
|
164
164
|
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
|
|
165
|
+
if (uiAuthConfig?.provider === 'external-token') {
|
|
166
|
+
applyExternalTokenFetchToGeneratedUi(modelInfo);
|
|
167
|
+
}
|
|
165
168
|
await updateNavigationMenu(modelInfo);
|
|
166
169
|
}
|
|
167
170
|
|
|
@@ -190,6 +193,31 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
|
|
197
|
+
const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
|
|
198
|
+
if (!fs.existsSync(uiRoot)) return;
|
|
199
|
+
const visit = (dir: string): void => {
|
|
200
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
201
|
+
const target = path.join(dir, entry.name);
|
|
202
|
+
if (entry.isDirectory()) { visit(target); continue; }
|
|
203
|
+
if (!entry.name.endsWith('.tsx')) continue;
|
|
204
|
+
let content = fs.readFileSync(target, 'utf-8');
|
|
205
|
+
if (!content.includes('fetch(')) continue;
|
|
206
|
+
content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
|
|
207
|
+
content = content.replace(
|
|
208
|
+
/(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
|
|
209
|
+
`if (res.status === 401) throw new Error('Authentication required');\n $1`
|
|
210
|
+
);
|
|
211
|
+
if (!content.includes("@/lib/auth/authenticated-fetch")) {
|
|
212
|
+
const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
|
|
213
|
+
content = `${directive}import { authenticatedFetch } from '@/lib/auth/authenticated-fetch';\n${content.slice(directive.length)}`;
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(target, content, 'utf-8');
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
visit(uiRoot);
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
/**
|
|
194
222
|
* モデルの authPolicy と auth config から実効的な認可ポリシーを解決
|
|
195
223
|
* - モデルに authPolicy があればそれを使用
|
|
@@ -305,8 +333,10 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
305
333
|
const hasAuth = authConfig && authConfig.provider !== 'none';
|
|
306
334
|
let helperCode: string;
|
|
307
335
|
if (hasAuth) {
|
|
308
|
-
const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
|
|
309
|
-
helperCode =
|
|
336
|
+
const { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithSwaAuth } = await import("../../core/scaffold/auth-generator");
|
|
337
|
+
helperCode = authConfig?.provider === "swa"
|
|
338
|
+
? generateBFFCallFunctionWithSwaAuth()
|
|
339
|
+
: generateBFFCallFunctionWithAuth();
|
|
310
340
|
} else {
|
|
311
341
|
helperCode = generateBFFCallFunction();
|
|
312
342
|
}
|
|
@@ -347,7 +377,7 @@ async function generateFunctionsCode(
|
|
|
347
377
|
fs.mkdirSync(functionDir, { recursive: true });
|
|
348
378
|
}
|
|
349
379
|
|
|
350
|
-
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
|
|
380
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
|
|
351
381
|
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
352
382
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
353
383
|
return;
|
|
@@ -425,14 +455,16 @@ async function generateConnectorFunctionsCode(
|
|
|
425
455
|
modelInfo, sharedPackageName,
|
|
426
456
|
connectorDef as RdbConnectorConfig,
|
|
427
457
|
connectorConfig as RdbModelConnectorConfig,
|
|
428
|
-
authPolicy
|
|
458
|
+
authPolicy,
|
|
459
|
+
authConfig?.provider
|
|
429
460
|
);
|
|
430
461
|
} else {
|
|
431
462
|
code = generateApiConnectorFunctionTS(
|
|
432
463
|
modelInfo, sharedPackageName,
|
|
433
464
|
connectorDef as ApiConnectorConfig,
|
|
434
465
|
connectorConfig as ApiModelConnectorConfig,
|
|
435
|
-
authPolicy
|
|
466
|
+
authPolicy,
|
|
467
|
+
authConfig?.provider
|
|
436
468
|
);
|
|
437
469
|
}
|
|
438
470
|
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
|
|
|
26
26
|
["--verbose", 0],
|
|
27
27
|
["--no-functions", 0],
|
|
28
28
|
["--mock-connectors", 0],
|
|
29
|
+
["--swa-port", 1],
|
|
30
|
+
["--no-swa", 0],
|
|
29
31
|
]);
|
|
30
32
|
|
|
31
33
|
function ensureUtf8ConsoleOnWindows(): void {
|
|
@@ -168,7 +170,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
168
170
|
program
|
|
169
171
|
.command("add-auth")
|
|
170
172
|
.description("Add authentication and authorization to the project")
|
|
171
|
-
.option("--provider <provider>", "Auth provider: custom-jwt | swa | swa-custom | none", "custom-jwt")
|
|
173
|
+
.option("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | swa-custom | none", "custom-jwt")
|
|
172
174
|
.action((options) => {
|
|
173
175
|
addAuthCommand({
|
|
174
176
|
provider: options.provider,
|