opennextjs-azure 0.1.1 → 0.1.3
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/README.md +24 -2
- package/dist/adapters/image-optimization.d.mts +20 -0
- package/dist/adapters/image-optimization.d.ts +20 -0
- package/dist/adapters/image-optimization.js +11 -0
- package/dist/adapters/wrappers/azure-functions.js +7 -4
- package/dist/adapters/wrappers/azure-image-optimization.d.mts +10 -0
- package/dist/adapters/wrappers/azure-image-optimization.d.ts +10 -0
- package/dist/adapters/wrappers/azure-image-optimization.js +151 -0
- package/dist/cli/index.js +486 -5
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +12 -0
- package/dist/deploy.js +182 -54
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/infrastructure/main.bicep +12 -0
- package/dist/overrides/imageLoader/azure-blob.d.mts +5 -0
- package/dist/overrides/imageLoader/azure-blob.d.ts +5 -0
- package/dist/overrides/imageLoader/azure-blob.js +37 -0
- package/dist/overrides/imageOptimization/azure-cached.d.mts +6 -0
- package/dist/overrides/imageOptimization/azure-cached.d.ts +6 -0
- package/dist/overrides/imageOptimization/azure-cached.js +115 -0
- package/dist/shared/{opennextjs-azure.d619537c.d.mts → opennextjs-azure.279bd73d.d.mts} +3 -1
- package/dist/shared/{opennextjs-azure.d619537c.d.ts → opennextjs-azure.279bd73d.d.ts} +3 -1
- package/infrastructure/main.bicep +12 -0
- package/package.json +2 -3
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { i as init, b as build, d as deploy } from '../deploy.js';
|
|
3
|
+
import { r as redX, g as greenCheck, p as promptForInput, i as init, b as build, d as deploy } from '../deploy.js';
|
|
4
4
|
import { exec } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
@@ -10,7 +10,7 @@ import '@opennextjs/aws/build.js';
|
|
|
10
10
|
import 'node:fs';
|
|
11
11
|
import 'node:readline';
|
|
12
12
|
|
|
13
|
-
const execAsync = promisify(exec);
|
|
13
|
+
const execAsync$2 = promisify(exec);
|
|
14
14
|
async function tail(options) {
|
|
15
15
|
const cwd = process.cwd();
|
|
16
16
|
const configPath = path.join(cwd, "azure.config.json");
|
|
@@ -34,13 +34,13 @@ async function tail(options) {
|
|
|
34
34
|
console.log(`\u{1F4E1} Opening Azure Portal Log Stream for ${functionAppName}...
|
|
35
35
|
`);
|
|
36
36
|
try {
|
|
37
|
-
const { stdout } = await execAsync("az account show --query '{tenant:tenantId, subscription:id}' -o json");
|
|
37
|
+
const { stdout } = await execAsync$2("az account show --query '{tenant:tenantId, subscription:id}' -o json");
|
|
38
38
|
const account = JSON.parse(stdout);
|
|
39
39
|
const portalUrl = `https://portal.azure.com/#@${account.tenant}/resource/subscriptions/${account.subscription}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${functionAppName}/logStream`;
|
|
40
40
|
console.log(`\u{1F310} Opening: ${portalUrl}
|
|
41
41
|
`);
|
|
42
42
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
43
|
-
await execAsync(`${openCommand} "${portalUrl}"`);
|
|
43
|
+
await execAsync$2(`${openCommand} "${portalUrl}"`);
|
|
44
44
|
console.log("\u2713 Log Stream page opened in your browser!");
|
|
45
45
|
console.log("\nTip: Enable Application Insights in azure.config.json for better log filtering.\n");
|
|
46
46
|
} catch (error) {
|
|
@@ -50,8 +50,483 @@ async function tail(options) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const execAsync$1 = promisify(exec);
|
|
54
|
+
const colors$1 = {
|
|
55
|
+
green: "\x1B[32m",
|
|
56
|
+
red: "\x1B[31m",
|
|
57
|
+
yellow: "\x1B[33m",
|
|
58
|
+
blue: "\x1B[34m",
|
|
59
|
+
reset: "\x1B[0m"
|
|
60
|
+
};
|
|
61
|
+
async function health(options) {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
console.log("Running health checks for Azure deployment...\n");
|
|
64
|
+
const configPath = path.join(cwd, "azure.config.json");
|
|
65
|
+
let config = {};
|
|
66
|
+
try {
|
|
67
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
68
|
+
config = JSON.parse(configContent);
|
|
69
|
+
} catch {
|
|
70
|
+
console.warn(`${colors$1.yellow}Warning:${colors$1.reset} azure.config.json not found.`);
|
|
71
|
+
console.warn(" Provide --app-name and --resource-group or run from a project directory.\n");
|
|
72
|
+
}
|
|
73
|
+
const appName = options?.appName || config.appName;
|
|
74
|
+
const resourceGroup = options?.resourceGroup || config.resourceGroup;
|
|
75
|
+
const environment = config.environment || "dev";
|
|
76
|
+
if (!appName || !resourceGroup) {
|
|
77
|
+
console.error(`${redX()} Missing required information!`);
|
|
78
|
+
console.error(" Provide --app-name and --resource-group or run from a project with azure.config.json\n");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const checks = [
|
|
82
|
+
{ name: "Azure CLI", fn: () => checkAzureCLI() },
|
|
83
|
+
{ name: "Azure Authentication", fn: () => checkAzureAuth() },
|
|
84
|
+
{ name: "Resource Group", fn: () => checkResourceGroup(resourceGroup) },
|
|
85
|
+
{ name: "Storage Account", fn: () => checkStorageAccount(appName, resourceGroup) },
|
|
86
|
+
{ name: "Function App", fn: () => checkFunctionApp(appName, resourceGroup, environment) },
|
|
87
|
+
{ name: "App Service Plan", fn: () => checkAppServicePlan(appName, resourceGroup, environment) },
|
|
88
|
+
{ name: "Function App Status", fn: () => checkFunctionAppStatus(appName, resourceGroup, environment) },
|
|
89
|
+
{ name: "Storage Containers", fn: () => checkStorageContainers(resourceGroup) },
|
|
90
|
+
{ name: "Function App Configuration", fn: () => checkFunctionAppConfig(appName, resourceGroup, environment) }
|
|
91
|
+
];
|
|
92
|
+
if (config.applicationInsights) {
|
|
93
|
+
checks.push({
|
|
94
|
+
name: "Application Insights",
|
|
95
|
+
fn: () => checkApplicationInsights(appName, resourceGroup, environment)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
let passedCount = 0;
|
|
99
|
+
let failedCount = 0;
|
|
100
|
+
const errors = [];
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
try {
|
|
103
|
+
const result = await check.fn();
|
|
104
|
+
if (result.passed) {
|
|
105
|
+
console.log(`${greenCheck()} ${check.name}`);
|
|
106
|
+
if (result.details) {
|
|
107
|
+
console.log(` ${result.details}`);
|
|
108
|
+
}
|
|
109
|
+
passedCount++;
|
|
110
|
+
} else {
|
|
111
|
+
console.log(`${redX()} ${check.name}`);
|
|
112
|
+
console.log(` ${result.message}`);
|
|
113
|
+
if (result.details) {
|
|
114
|
+
console.log(` ${result.details}`);
|
|
115
|
+
}
|
|
116
|
+
failedCount++;
|
|
117
|
+
errors.push(`${check.name}: ${result.message}`);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.log(`${redX()} ${check.name}`);
|
|
121
|
+
console.log(` ${error.message}`);
|
|
122
|
+
failedCount++;
|
|
123
|
+
errors.push(`${check.name}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
127
|
+
if (failedCount === 0) {
|
|
128
|
+
console.log(`${colors$1.green}All checks passed!${colors$1.reset}`);
|
|
129
|
+
console.log(` ${passedCount}/${checks.length} checks successful`);
|
|
130
|
+
} else {
|
|
131
|
+
console.log(`${colors$1.yellow}Some checks failed${colors$1.reset}`);
|
|
132
|
+
console.log(` ${passedCount}/${checks.length} checks passed`);
|
|
133
|
+
console.log(` ${failedCount}/${checks.length} checks failed`);
|
|
134
|
+
}
|
|
135
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
|
|
136
|
+
if (failedCount > 0) {
|
|
137
|
+
console.log("Issues detected:");
|
|
138
|
+
errors.forEach((error) => console.log(` \u2022 ${error}`));
|
|
139
|
+
console.log("\nRun 'opennextjs-azure deploy' to provision or repair infrastructure.\n");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function checkAzureCLI() {
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await execAsync$1("az --version");
|
|
146
|
+
const versionMatch = stdout.match(/azure-cli\s+(\S+)/);
|
|
147
|
+
const version = versionMatch ? versionMatch[1] : "unknown";
|
|
148
|
+
return {
|
|
149
|
+
passed: true,
|
|
150
|
+
message: "Azure CLI installed",
|
|
151
|
+
details: `Version: ${version}`
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return {
|
|
155
|
+
passed: false,
|
|
156
|
+
message: "Azure CLI not found",
|
|
157
|
+
details: "Install from: https://docs.microsoft.com/cli/azure/install-azure-cli"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function checkAzureAuth() {
|
|
162
|
+
try {
|
|
163
|
+
const { stdout } = await execAsync$1("az account show --query '{Name:name, Id:id, State:state}' -o json");
|
|
164
|
+
const account = JSON.parse(stdout);
|
|
165
|
+
if (account.State !== "Enabled") {
|
|
166
|
+
return {
|
|
167
|
+
passed: false,
|
|
168
|
+
message: `Subscription state: ${account.State}`,
|
|
169
|
+
details: "Expected: Enabled"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
passed: true,
|
|
174
|
+
message: "Authenticated to Azure",
|
|
175
|
+
details: `Subscription: ${account.Name}`
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
return {
|
|
179
|
+
passed: false,
|
|
180
|
+
message: "Not logged in to Azure",
|
|
181
|
+
details: "Run: az login"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function checkResourceGroup(resourceGroup) {
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execAsync$1(
|
|
188
|
+
`az group show --name ${resourceGroup} --query '{Location:location, State:properties.provisioningState}' -o json`
|
|
189
|
+
);
|
|
190
|
+
const rg = JSON.parse(stdout);
|
|
191
|
+
return {
|
|
192
|
+
passed: true,
|
|
193
|
+
message: "Resource group exists",
|
|
194
|
+
details: `Location: ${rg.Location}, State: ${rg.State}`
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return {
|
|
198
|
+
passed: false,
|
|
199
|
+
message: `Resource group "${resourceGroup}" not found`,
|
|
200
|
+
details: "Run 'opennextjs-azure deploy' to create it"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function checkStorageAccount(appName, resourceGroup, environment) {
|
|
205
|
+
try {
|
|
206
|
+
const { stdout } = await execAsync$1(
|
|
207
|
+
`az storage account list --resource-group ${resourceGroup} --query "[?starts_with(name, '${appName.replace(/-/g, "").substring(0, 10)}')].{Name:name, Location:location, Sku:sku.name, Status:statusOfPrimary}" -o json`
|
|
208
|
+
);
|
|
209
|
+
const accounts = JSON.parse(stdout);
|
|
210
|
+
if (accounts.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
passed: false,
|
|
213
|
+
message: "Storage account not found",
|
|
214
|
+
details: `Expected storage account starting with "${appName.replace(/-/g, "").substring(0, 10)}"`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const account = accounts[0];
|
|
218
|
+
if (account.Status !== "available") {
|
|
219
|
+
return {
|
|
220
|
+
passed: false,
|
|
221
|
+
message: "Storage account not available",
|
|
222
|
+
details: `Status: ${account.Status}`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const { stdout: staticFileStdout } = await execAsync$1(
|
|
227
|
+
`az storage blob list --account-name ${account.Name} --container-name assets --prefix _next/static --query '[0].name' -o tsv --only-show-errors 2>/dev/null || echo ""`
|
|
228
|
+
);
|
|
229
|
+
const sampleFile = staticFileStdout.trim();
|
|
230
|
+
if (sampleFile) {
|
|
231
|
+
const { stdout: cacheStdout } = await execAsync$1(
|
|
232
|
+
`az storage blob show --account-name ${account.Name} --container-name assets --name '${sampleFile}' --query 'properties.contentSettings.cacheControl' -o tsv --only-show-errors 2>/dev/null || echo ""`
|
|
233
|
+
);
|
|
234
|
+
const cacheControl = cacheStdout.trim();
|
|
235
|
+
const expectedCache = "public, max-age=31536000, immutable";
|
|
236
|
+
if (cacheControl !== expectedCache) {
|
|
237
|
+
return {
|
|
238
|
+
passed: false,
|
|
239
|
+
message: "Static asset cache headers misconfigured",
|
|
240
|
+
details: `${sampleFile.split("/").pop()} has '${cacheControl || "none"}' (expected '${expectedCache}')`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
passed: true,
|
|
248
|
+
message: "Storage account healthy",
|
|
249
|
+
details: `${account.Name} (${account.Sku})`
|
|
250
|
+
};
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return {
|
|
253
|
+
passed: false,
|
|
254
|
+
message: "Failed to check storage account",
|
|
255
|
+
details: error.message
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function checkFunctionApp(appName, resourceGroup, environment) {
|
|
260
|
+
const functionAppName = `${appName}-func-${environment}`;
|
|
261
|
+
try {
|
|
262
|
+
const { stdout } = await execAsync$1(
|
|
263
|
+
`az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query '{Name:name, State:state, Kind:kind}' -o json`
|
|
264
|
+
);
|
|
265
|
+
const funcApp = JSON.parse(stdout);
|
|
266
|
+
return {
|
|
267
|
+
passed: true,
|
|
268
|
+
message: "Function app exists",
|
|
269
|
+
details: `${funcApp.Name} (${funcApp.Kind})`
|
|
270
|
+
};
|
|
271
|
+
} catch {
|
|
272
|
+
return {
|
|
273
|
+
passed: false,
|
|
274
|
+
message: `Function app "${functionAppName}" not found`,
|
|
275
|
+
details: "Run 'opennextjs-azure deploy' to create it"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function checkAppServicePlan(appName, resourceGroup, environment) {
|
|
280
|
+
const planName = `${appName}-plan-${environment}`;
|
|
281
|
+
try {
|
|
282
|
+
const { stdout } = await execAsync$1(
|
|
283
|
+
`az appservice plan show --resource-group ${resourceGroup} --name ${planName} --query '{Name:name, Sku:sku.name, Tier:sku.tier, Status:properties.status}' -o json`
|
|
284
|
+
);
|
|
285
|
+
const plan = JSON.parse(stdout);
|
|
286
|
+
if (plan.Status !== "Ready") {
|
|
287
|
+
return {
|
|
288
|
+
passed: false,
|
|
289
|
+
message: "App Service Plan not ready",
|
|
290
|
+
details: `Status: ${plan.Status}`
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
passed: true,
|
|
295
|
+
message: "App Service Plan healthy",
|
|
296
|
+
details: `${plan.Tier} (${plan.Sku})`
|
|
297
|
+
};
|
|
298
|
+
} catch {
|
|
299
|
+
return {
|
|
300
|
+
passed: false,
|
|
301
|
+
message: `App Service Plan "${planName}" not found`,
|
|
302
|
+
details: "Run 'opennextjs-azure deploy' to create it"
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function checkFunctionAppStatus(appName, resourceGroup, environment) {
|
|
307
|
+
const functionAppName = `${appName}-func-${environment}`;
|
|
308
|
+
try {
|
|
309
|
+
const { stdout } = await execAsync$1(
|
|
310
|
+
`az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query '{State:state, DefaultHostName:defaultHostName, OutboundIpAddresses:outboundIpAddresses}' -o json`
|
|
311
|
+
);
|
|
312
|
+
const funcApp = JSON.parse(stdout);
|
|
313
|
+
if (funcApp.State !== "Running") {
|
|
314
|
+
return {
|
|
315
|
+
passed: false,
|
|
316
|
+
message: "Function app not running",
|
|
317
|
+
details: `State: ${funcApp.State}`
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const url = `https://${funcApp.DefaultHostName}`;
|
|
321
|
+
try {
|
|
322
|
+
const startTime = Date.now();
|
|
323
|
+
await execAsync$1(`curl -I -s -o /dev/null -w "%{http_code}" --max-time 10 "${url}" || echo "000"`);
|
|
324
|
+
const responseTime = Date.now() - startTime;
|
|
325
|
+
return {
|
|
326
|
+
passed: true,
|
|
327
|
+
message: "Function app running and responding",
|
|
328
|
+
details: `URL: ${url} (${responseTime}ms)`
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return {
|
|
332
|
+
passed: true,
|
|
333
|
+
message: "Function app running",
|
|
334
|
+
details: `URL: ${url} (HTTP check failed, but app is running)`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return {
|
|
339
|
+
passed: false,
|
|
340
|
+
message: "Failed to check function app status",
|
|
341
|
+
details: error.message
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function checkStorageContainers(resourceGroup) {
|
|
346
|
+
try {
|
|
347
|
+
const { stdout: accountStdout } = await execAsync$1(
|
|
348
|
+
`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
|
|
349
|
+
);
|
|
350
|
+
const storageAccountName = accountStdout.trim();
|
|
351
|
+
const { stdout: containersStdout } = await execAsync$1(
|
|
352
|
+
`az storage container list --account-name ${storageAccountName} --query "[].name" -o json --only-show-errors`
|
|
353
|
+
);
|
|
354
|
+
const containers = JSON.parse(containersStdout);
|
|
355
|
+
const requiredContainers = ["assets", "nextjs-cache"];
|
|
356
|
+
const missingContainers = requiredContainers.filter((c) => !containers.includes(c));
|
|
357
|
+
if (missingContainers.length > 0) {
|
|
358
|
+
return {
|
|
359
|
+
passed: false,
|
|
360
|
+
message: "Missing storage containers",
|
|
361
|
+
details: `Missing: ${missingContainers.join(", ")}`
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
passed: true,
|
|
366
|
+
message: "Storage containers configured",
|
|
367
|
+
details: `Found: ${containers.length} container(s)`
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
passed: false,
|
|
372
|
+
message: "Failed to check storage containers",
|
|
373
|
+
details: error.message
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function checkFunctionAppConfig(appName, resourceGroup, environment) {
|
|
378
|
+
const functionAppName = `${appName}-func-${environment}`;
|
|
379
|
+
try {
|
|
380
|
+
const { stdout } = await execAsync$1(
|
|
381
|
+
`az functionapp config appsettings list --resource-group ${resourceGroup} --name ${functionAppName} --query "[?name=='AZURE_STORAGE_CONNECTION_STRING' || name=='FUNCTIONS_WORKER_RUNTIME'].{Name:name, Value:value}" -o json`
|
|
382
|
+
);
|
|
383
|
+
const settings = JSON.parse(stdout);
|
|
384
|
+
const hasStorageConnection = settings.some((s) => s.Name === "AZURE_STORAGE_CONNECTION_STRING");
|
|
385
|
+
const hasRuntime = settings.some((s) => s.Name === "FUNCTIONS_WORKER_RUNTIME");
|
|
386
|
+
if (!hasStorageConnection || !hasRuntime) {
|
|
387
|
+
const missing = [];
|
|
388
|
+
if (!hasStorageConnection)
|
|
389
|
+
missing.push("AZURE_STORAGE_CONNECTION_STRING");
|
|
390
|
+
if (!hasRuntime)
|
|
391
|
+
missing.push("FUNCTIONS_WORKER_RUNTIME");
|
|
392
|
+
return {
|
|
393
|
+
passed: false,
|
|
394
|
+
message: "Missing required configuration",
|
|
395
|
+
details: `Missing: ${missing.join(", ")}`
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
passed: true,
|
|
400
|
+
message: "Function app configured",
|
|
401
|
+
details: `${settings.length} setting(s) verified`
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return {
|
|
405
|
+
passed: false,
|
|
406
|
+
message: "Failed to check function app configuration",
|
|
407
|
+
details: error.message
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function checkApplicationInsights(appName, resourceGroup, environment) {
|
|
412
|
+
const insightsName = `${appName}-insights-${environment}`;
|
|
413
|
+
try {
|
|
414
|
+
const { stdout } = await execAsync$1(
|
|
415
|
+
`az monitor app-insights component show --app ${insightsName} --resource-group ${resourceGroup} --query '{Name:name, ApplicationType:applicationType, ProvisioningState:provisioningState}' -o json`
|
|
416
|
+
);
|
|
417
|
+
const insights = JSON.parse(stdout);
|
|
418
|
+
if (insights.ProvisioningState !== "Succeeded") {
|
|
419
|
+
return {
|
|
420
|
+
passed: false,
|
|
421
|
+
message: "Application Insights not ready",
|
|
422
|
+
details: `State: ${insights.ProvisioningState}`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
passed: true,
|
|
427
|
+
message: "Application Insights configured",
|
|
428
|
+
details: `${insights.Name} (${insights.ApplicationType})`
|
|
429
|
+
};
|
|
430
|
+
} catch {
|
|
431
|
+
return {
|
|
432
|
+
passed: false,
|
|
433
|
+
message: `Application Insights "${insightsName}" not found`,
|
|
434
|
+
details: "Enable in azure.config.json and redeploy"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const execAsync = promisify(exec);
|
|
440
|
+
const colors = {
|
|
441
|
+
red: "\x1B[31m",
|
|
442
|
+
yellow: "\x1B[33m",
|
|
443
|
+
reset: "\x1B[0m"
|
|
444
|
+
};
|
|
445
|
+
async function deleteResourceGroup(options) {
|
|
446
|
+
const cwd = process.cwd();
|
|
447
|
+
let resourceGroup = options.resourceGroup;
|
|
448
|
+
if (!resourceGroup) {
|
|
449
|
+
const configPath = path.join(cwd, "azure.config.json");
|
|
450
|
+
try {
|
|
451
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
452
|
+
const config = JSON.parse(configContent);
|
|
453
|
+
resourceGroup = config.resourceGroup;
|
|
454
|
+
} catch {
|
|
455
|
+
console.error(`${redX()} No resource group specified!`);
|
|
456
|
+
console.error(" Provide --resource-group or run from a project with azure.config.json\n");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!resourceGroup) {
|
|
461
|
+
console.error(`${redX()} No resource group specified!`);
|
|
462
|
+
console.error(" Provide --resource-group\n");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const { stdout } = await execAsync(
|
|
467
|
+
`az group show --name ${resourceGroup} --query '{Location:location, State:properties.provisioningState}' -o json`
|
|
468
|
+
);
|
|
469
|
+
const rg = JSON.parse(stdout);
|
|
470
|
+
console.log(`
|
|
471
|
+
${colors.yellow}\u26A0\uFE0F WARNING: You are about to delete the resource group:${colors.reset}`);
|
|
472
|
+
console.log(` Resource Group: ${resourceGroup}`);
|
|
473
|
+
console.log(` Location: ${rg.Location}`);
|
|
474
|
+
console.log(` State: ${rg.State}`);
|
|
475
|
+
console.log(`
|
|
476
|
+
${colors.red}This will permanently delete ALL resources in this resource group!${colors.reset}`);
|
|
477
|
+
console.log(" This action cannot be undone.\n");
|
|
478
|
+
} catch {
|
|
479
|
+
console.error(`${redX()} Resource group "${resourceGroup}" not found!
|
|
480
|
+
`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
if (!options.yes) {
|
|
484
|
+
const confirmation = await promptForInput(
|
|
485
|
+
`Type the resource group name "${resourceGroup}" to confirm deletion: `
|
|
486
|
+
);
|
|
487
|
+
if (confirmation !== resourceGroup) {
|
|
488
|
+
console.log(`
|
|
489
|
+
${redX()} Deletion cancelled. Resource group name did not match.
|
|
490
|
+
`);
|
|
491
|
+
process.exit(0);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
console.log(`
|
|
495
|
+
Deleting resource group "${resourceGroup}"...`);
|
|
496
|
+
if (options.noWait) {
|
|
497
|
+
console.log("Initiating deletion in the background...\n");
|
|
498
|
+
try {
|
|
499
|
+
await execAsync(`az group delete --name ${resourceGroup} --yes --no-wait`);
|
|
500
|
+
console.log(`${greenCheck()} Resource group deletion initiated successfully!`);
|
|
501
|
+
console.log(" Azure is now deleting the resource group in the background.");
|
|
502
|
+
console.log(`
|
|
503
|
+
${colors.yellow}Note:${colors.reset} Deletion typically takes 3-5 minutes to complete.`);
|
|
504
|
+
console.log(` The resource group name "${resourceGroup}" cannot be reused until deletion finishes.`);
|
|
505
|
+
console.log(` Run 'az group show --name ${resourceGroup}' to check deletion status.
|
|
506
|
+
`);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error(`${redX()} Failed to delete resource group!`);
|
|
509
|
+
console.error(` ${error.message}
|
|
510
|
+
`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
console.log("This will take a few minutes. Please wait...\n");
|
|
515
|
+
try {
|
|
516
|
+
await execAsync(`az group delete --name ${resourceGroup} --yes`);
|
|
517
|
+
console.log(`${greenCheck()} Resource group "${resourceGroup}" deleted successfully!`);
|
|
518
|
+
console.log(" All resources have been removed.\n");
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error(`${redX()} Failed to delete resource group!`);
|
|
521
|
+
console.error(` ${error.message}
|
|
522
|
+
`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
53
528
|
const program = new Command();
|
|
54
|
-
program.name("opennextjs-azure").description("CLI tool for building and deploying Next.js apps to Azure").version("0.1.
|
|
529
|
+
program.name("opennextjs-azure").description("CLI tool for building and deploying Next.js apps to Azure").version("0.1.2");
|
|
55
530
|
program.command("init").description("Initialize Azure infrastructure in your project").option("--scaffold", "Scaffold a new Next.js project if in empty directory").option("--no-typescript", "Disable TypeScript (default: enabled)").option("--no-tailwind", "Disable Tailwind CSS (default: enabled with v3)").option("--no-eslint", "Disable ESLint (default: enabled)").option("--no-src-dir", "Disable src/ directory (default: enabled)").option("--no-app-router", "Use Pages Router instead of App Router").option("--import-alias <alias>", "Import alias (default: @/*)").option("--package-manager <pm>", "Package manager: npm, yarn, pnpm, bun (default: pnpm)").action(async (options) => {
|
|
56
531
|
await init(options);
|
|
57
532
|
});
|
|
@@ -64,4 +539,10 @@ program.command("deploy").description("Deploy Next.js app to Azure (provisions i
|
|
|
64
539
|
program.command("tail").description("Open Azure Portal Log Stream in browser (live logs)").option("-n, --app-name <name>", "Application name").option("-g, --resource-group <name>", "Azure resource group name").action(async (options) => {
|
|
65
540
|
await tail(options);
|
|
66
541
|
});
|
|
542
|
+
program.command("health").description("Check health status of Azure deployment resources").option("-n, --app-name <name>", "Application name").option("-g, --resource-group <name>", "Azure resource group name").action(async (options) => {
|
|
543
|
+
await health(options);
|
|
544
|
+
});
|
|
545
|
+
program.command("delete").description("Delete Azure resource group and all its resources").option("-g, --resource-group <name>", "Azure resource group name").option("-y, --yes", "Skip confirmation prompt").option("--no-wait", "Delete in background (returns immediately)").action(async (options) => {
|
|
546
|
+
await deleteResourceGroup(options);
|
|
547
|
+
});
|
|
67
548
|
program.parse();
|
package/dist/config/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@opennextjs/aws/types/open-next.js';
|
|
2
|
-
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.
|
|
2
|
+
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.mjs';
|
|
3
3
|
import '@opennextjs/aws/types/overrides.js';
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@opennextjs/aws/types/open-next.js';
|
|
2
|
-
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.
|
|
2
|
+
export { d as defineAzureConfig, g as getAzureConfig } from '../shared/opennextjs-azure.279bd73d.js';
|
|
3
3
|
import '@opennextjs/aws/types/overrides.js';
|
package/dist/config/index.js
CHANGED
|
@@ -14,6 +14,9 @@ function defineAzureConfig(config = {}) {
|
|
|
14
14
|
middleware: config.middleware || {
|
|
15
15
|
external: false
|
|
16
16
|
},
|
|
17
|
+
imageOptimization: {
|
|
18
|
+
loader: resolveImageLoader(config.imageLoader)
|
|
19
|
+
},
|
|
17
20
|
dangerous: config.dangerous,
|
|
18
21
|
buildCommand: config.buildCommand,
|
|
19
22
|
buildOutputPath: config.buildOutputPath || ".",
|
|
@@ -48,6 +51,15 @@ function resolveQueue(value) {
|
|
|
48
51
|
}
|
|
49
52
|
return () => value;
|
|
50
53
|
}
|
|
54
|
+
function resolveImageLoader(value) {
|
|
55
|
+
if (!value || value === "azure-blob") {
|
|
56
|
+
return () => import('../overrides/imageLoader/azure-blob.js').then((m) => m.default);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "function") {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return () => value;
|
|
62
|
+
}
|
|
51
63
|
function getAzureConfig() {
|
|
52
64
|
return {
|
|
53
65
|
deployment: {
|