opennextjs-azure 0.1.1 → 0.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenNext.js Azure
2
2
 
3
- **True serverless Next.js on Azure Functions**
3
+ **True serverless Next.js on Azure Functions** (EXPERIMENTAL)
4
4
 
5
5
  [![NPM Version](https://img.shields.io/npm/v/opennextjs-azure)](https://www.npmjs.com/package/opennextjs-azure)
6
6
  [![NPM Downloads](https://img.shields.io/npm/dt/opennextjs-azure)](https://www.npmjs.com/package/opennextjs-azure)
@@ -8,6 +8,8 @@
8
8
 
9
9
  Built on the [OpenNext](https://opennext.js.org) framework, this adapter brings native Next.js support to Azure Functions.
10
10
 
11
+ ![Deploy Screenshot](./docs/deploy-screenshot.png)
12
+
11
13
  > **🚀 New to Azure deployment?** Jump to [Quick Start](#quick-start) and run `npx opennextjs-azure@latest init --scaffold` to create a fully configured Next.js app on Azure in seconds!
12
14
 
13
15
  ## The Gap This Project Fills
@@ -118,6 +120,8 @@ Response Stream → Azure Functions Response
118
120
  - Environment variables
119
121
  - Connection strings
120
122
 
123
+ ![Resource Group Screenshot](./docs/rg-screenshot.png)
124
+
121
125
  Choose your environment:
122
126
 
123
127
  - `--environment dev` → Y1 Consumption (pay-per-execution)
@@ -183,6 +187,17 @@ opennextjs-azure deploy \
183
187
  opennextjs-azure tail \
184
188
  [--app-name <name>] \
185
189
  [--resource-group <name>]
190
+
191
+ # Check deployment health
192
+ opennextjs-azure health \
193
+ [--app-name <name>] \
194
+ [--resource-group <name>]
195
+
196
+ # Delete resource group and all resources
197
+ opennextjs-azure delete \
198
+ [--resource-group <name>] \
199
+ [--yes] \
200
+ [--no-wait]
186
201
  ```
187
202
 
188
203
  ## License
@@ -82,13 +82,16 @@ const handler = async (handler2, converter) => async (context, request) => {
82
82
  };
83
83
  }
84
84
  } catch (error) {
85
+ const isProduction = process.env.NODE_ENV === "production";
85
86
  context.res = {
86
87
  status: 500,
87
88
  headers: { "content-type": "application/json" },
88
89
  body: JSON.stringify({
89
90
  error: "Internal Server Error",
90
- message: error instanceof Error ? error.message : String(error),
91
- stack: error instanceof Error ? error.stack : void 0
91
+ ...isProduction ? {} : {
92
+ message: error instanceof Error ? error.message : String(error),
93
+ stack: error instanceof Error ? error.stack : void 0
94
+ }
92
95
  })
93
96
  };
94
97
  }
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,450 @@ 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
+ return {
226
+ passed: true,
227
+ message: "Storage account healthy",
228
+ details: `${account.Name} (${account.Sku})`
229
+ };
230
+ } catch (error) {
231
+ return {
232
+ passed: false,
233
+ message: "Failed to check storage account",
234
+ details: error.message
235
+ };
236
+ }
237
+ }
238
+ async function checkFunctionApp(appName, resourceGroup, environment) {
239
+ const functionAppName = `${appName}-func-${environment}`;
240
+ try {
241
+ const { stdout } = await execAsync$1(
242
+ `az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query '{Name:name, State:state, Kind:kind}' -o json`
243
+ );
244
+ const funcApp = JSON.parse(stdout);
245
+ return {
246
+ passed: true,
247
+ message: "Function app exists",
248
+ details: `${funcApp.Name} (${funcApp.Kind})`
249
+ };
250
+ } catch {
251
+ return {
252
+ passed: false,
253
+ message: `Function app "${functionAppName}" not found`,
254
+ details: "Run 'opennextjs-azure deploy' to create it"
255
+ };
256
+ }
257
+ }
258
+ async function checkAppServicePlan(appName, resourceGroup, environment) {
259
+ const planName = `${appName}-plan-${environment}`;
260
+ try {
261
+ const { stdout } = await execAsync$1(
262
+ `az appservice plan show --resource-group ${resourceGroup} --name ${planName} --query '{Name:name, Sku:sku.name, Tier:sku.tier, Status:properties.status}' -o json`
263
+ );
264
+ const plan = JSON.parse(stdout);
265
+ if (plan.Status !== "Ready") {
266
+ return {
267
+ passed: false,
268
+ message: "App Service Plan not ready",
269
+ details: `Status: ${plan.Status}`
270
+ };
271
+ }
272
+ return {
273
+ passed: true,
274
+ message: "App Service Plan healthy",
275
+ details: `${plan.Tier} (${plan.Sku})`
276
+ };
277
+ } catch {
278
+ return {
279
+ passed: false,
280
+ message: `App Service Plan "${planName}" not found`,
281
+ details: "Run 'opennextjs-azure deploy' to create it"
282
+ };
283
+ }
284
+ }
285
+ async function checkFunctionAppStatus(appName, resourceGroup, environment) {
286
+ const functionAppName = `${appName}-func-${environment}`;
287
+ try {
288
+ const { stdout } = await execAsync$1(
289
+ `az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query '{State:state, DefaultHostName:defaultHostName, OutboundIpAddresses:outboundIpAddresses}' -o json`
290
+ );
291
+ const funcApp = JSON.parse(stdout);
292
+ if (funcApp.State !== "Running") {
293
+ return {
294
+ passed: false,
295
+ message: "Function app not running",
296
+ details: `State: ${funcApp.State}`
297
+ };
298
+ }
299
+ return {
300
+ passed: true,
301
+ message: "Function app running",
302
+ details: `URL: https://${funcApp.DefaultHostName}`
303
+ };
304
+ } catch (error) {
305
+ return {
306
+ passed: false,
307
+ message: "Failed to check function app status",
308
+ details: error.message
309
+ };
310
+ }
311
+ }
312
+ async function checkStorageContainers(resourceGroup) {
313
+ try {
314
+ const { stdout: accountStdout } = await execAsync$1(
315
+ `az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
316
+ );
317
+ const storageAccountName = accountStdout.trim();
318
+ const { stdout: containersStdout } = await execAsync$1(
319
+ `az storage container list --account-name ${storageAccountName} --query "[].name" -o json --only-show-errors`
320
+ );
321
+ const containers = JSON.parse(containersStdout);
322
+ const requiredContainers = ["assets", "nextjs-cache"];
323
+ const missingContainers = requiredContainers.filter((c) => !containers.includes(c));
324
+ if (missingContainers.length > 0) {
325
+ return {
326
+ passed: false,
327
+ message: "Missing storage containers",
328
+ details: `Missing: ${missingContainers.join(", ")}`
329
+ };
330
+ }
331
+ return {
332
+ passed: true,
333
+ message: "Storage containers configured",
334
+ details: `Found: ${containers.length} container(s)`
335
+ };
336
+ } catch (error) {
337
+ return {
338
+ passed: false,
339
+ message: "Failed to check storage containers",
340
+ details: error.message
341
+ };
342
+ }
343
+ }
344
+ async function checkFunctionAppConfig(appName, resourceGroup, environment) {
345
+ const functionAppName = `${appName}-func-${environment}`;
346
+ try {
347
+ const { stdout } = await execAsync$1(
348
+ `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`
349
+ );
350
+ const settings = JSON.parse(stdout);
351
+ const hasStorageConnection = settings.some((s) => s.Name === "AZURE_STORAGE_CONNECTION_STRING");
352
+ const hasRuntime = settings.some((s) => s.Name === "FUNCTIONS_WORKER_RUNTIME");
353
+ if (!hasStorageConnection || !hasRuntime) {
354
+ const missing = [];
355
+ if (!hasStorageConnection)
356
+ missing.push("AZURE_STORAGE_CONNECTION_STRING");
357
+ if (!hasRuntime)
358
+ missing.push("FUNCTIONS_WORKER_RUNTIME");
359
+ return {
360
+ passed: false,
361
+ message: "Missing required configuration",
362
+ details: `Missing: ${missing.join(", ")}`
363
+ };
364
+ }
365
+ return {
366
+ passed: true,
367
+ message: "Function app configured",
368
+ details: `${settings.length} setting(s) verified`
369
+ };
370
+ } catch (error) {
371
+ return {
372
+ passed: false,
373
+ message: "Failed to check function app configuration",
374
+ details: error.message
375
+ };
376
+ }
377
+ }
378
+ async function checkApplicationInsights(appName, resourceGroup, environment) {
379
+ const insightsName = `${appName}-insights-${environment}`;
380
+ try {
381
+ const { stdout } = await execAsync$1(
382
+ `az monitor app-insights component show --app ${insightsName} --resource-group ${resourceGroup} --query '{Name:name, ApplicationType:applicationType, ProvisioningState:provisioningState}' -o json`
383
+ );
384
+ const insights = JSON.parse(stdout);
385
+ if (insights.ProvisioningState !== "Succeeded") {
386
+ return {
387
+ passed: false,
388
+ message: "Application Insights not ready",
389
+ details: `State: ${insights.ProvisioningState}`
390
+ };
391
+ }
392
+ return {
393
+ passed: true,
394
+ message: "Application Insights configured",
395
+ details: `${insights.Name} (${insights.ApplicationType})`
396
+ };
397
+ } catch {
398
+ return {
399
+ passed: false,
400
+ message: `Application Insights "${insightsName}" not found`,
401
+ details: "Enable in azure.config.json and redeploy"
402
+ };
403
+ }
404
+ }
405
+
406
+ const execAsync = promisify(exec);
407
+ const colors = {
408
+ red: "\x1B[31m",
409
+ yellow: "\x1B[33m",
410
+ reset: "\x1B[0m"
411
+ };
412
+ async function deleteResourceGroup(options) {
413
+ const cwd = process.cwd();
414
+ let resourceGroup = options.resourceGroup;
415
+ if (!resourceGroup) {
416
+ const configPath = path.join(cwd, "azure.config.json");
417
+ try {
418
+ const configContent = await fs.readFile(configPath, "utf-8");
419
+ const config = JSON.parse(configContent);
420
+ resourceGroup = config.resourceGroup;
421
+ } catch {
422
+ console.error(`${redX()} No resource group specified!`);
423
+ console.error(" Provide --resource-group or run from a project with azure.config.json\n");
424
+ process.exit(1);
425
+ }
426
+ }
427
+ if (!resourceGroup) {
428
+ console.error(`${redX()} No resource group specified!`);
429
+ console.error(" Provide --resource-group\n");
430
+ process.exit(1);
431
+ }
432
+ try {
433
+ const { stdout } = await execAsync(
434
+ `az group show --name ${resourceGroup} --query '{Location:location, State:properties.provisioningState}' -o json`
435
+ );
436
+ const rg = JSON.parse(stdout);
437
+ console.log(`
438
+ ${colors.yellow}\u26A0\uFE0F WARNING: You are about to delete the resource group:${colors.reset}`);
439
+ console.log(` Resource Group: ${resourceGroup}`);
440
+ console.log(` Location: ${rg.Location}`);
441
+ console.log(` State: ${rg.State}`);
442
+ console.log(`
443
+ ${colors.red}This will permanently delete ALL resources in this resource group!${colors.reset}`);
444
+ console.log(" This action cannot be undone.\n");
445
+ } catch {
446
+ console.error(`${redX()} Resource group "${resourceGroup}" not found!
447
+ `);
448
+ process.exit(1);
449
+ }
450
+ if (!options.yes) {
451
+ const confirmation = await promptForInput(
452
+ `Type the resource group name "${resourceGroup}" to confirm deletion: `
453
+ );
454
+ if (confirmation !== resourceGroup) {
455
+ console.log(`
456
+ ${redX()} Deletion cancelled. Resource group name did not match.
457
+ `);
458
+ process.exit(0);
459
+ }
460
+ }
461
+ console.log(`
462
+ Deleting resource group "${resourceGroup}"...`);
463
+ if (options.noWait) {
464
+ console.log("Initiating deletion in the background...\n");
465
+ try {
466
+ await execAsync(`az group delete --name ${resourceGroup} --yes --no-wait`);
467
+ console.log(`${greenCheck()} Resource group deletion initiated successfully!`);
468
+ console.log(" Azure is now deleting the resource group in the background.");
469
+ console.log(`
470
+ ${colors.yellow}Note:${colors.reset} Deletion typically takes 3-5 minutes to complete.`);
471
+ console.log(` The resource group name "${resourceGroup}" cannot be reused until deletion finishes.`);
472
+ console.log(` Run 'az group show --name ${resourceGroup}' to check deletion status.
473
+ `);
474
+ } catch (error) {
475
+ console.error(`${redX()} Failed to delete resource group!`);
476
+ console.error(` ${error.message}
477
+ `);
478
+ process.exit(1);
479
+ }
480
+ } else {
481
+ console.log("This will take a few minutes. Please wait...\n");
482
+ try {
483
+ await execAsync(`az group delete --name ${resourceGroup} --yes`);
484
+ console.log(`${greenCheck()} Resource group "${resourceGroup}" deleted successfully!`);
485
+ console.log(" All resources have been removed.\n");
486
+ } catch (error) {
487
+ console.error(`${redX()} Failed to delete resource group!`);
488
+ console.error(` ${error.message}
489
+ `);
490
+ process.exit(1);
491
+ }
492
+ }
493
+ }
494
+
53
495
  const program = new Command();
54
- program.name("opennextjs-azure").description("CLI tool for building and deploying Next.js apps to Azure").version("0.1.0");
496
+ program.name("opennextjs-azure").description("CLI tool for building and deploying Next.js apps to Azure").version("0.1.2");
55
497
  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
498
  await init(options);
57
499
  });
@@ -64,4 +506,10 @@ program.command("deploy").description("Deploy Next.js app to Azure (provisions i
64
506
  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
507
  await tail(options);
66
508
  });
509
+ 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) => {
510
+ await health(options);
511
+ });
512
+ 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) => {
513
+ await deleteResourceGroup(options);
514
+ });
67
515
  program.parse();
package/dist/deploy.js CHANGED
@@ -166,6 +166,19 @@ function getAzureConfigTemplate() {
166
166
  `;
167
167
  }
168
168
 
169
+ const colors$1 = {
170
+ green: "\x1B[32m",
171
+ red: "\x1B[31m",
172
+ yellow: "\x1B[33m",
173
+ reset: "\x1B[0m"
174
+ };
175
+ function greenCheck() {
176
+ return `${colors$1.green}\u2713${colors$1.reset}`;
177
+ }
178
+ function redX() {
179
+ return `${colors$1.red}\u2717${colors$1.reset}`;
180
+ }
181
+
169
182
  const execAsync$2 = promisify(exec);
170
183
  async function prepareFunctions() {
171
184
  const functionsDir = path.join(process.cwd(), ".open-next/server-functions/default");
@@ -240,8 +253,9 @@ async function prepareFunctions() {
240
253
  entryPoint: "handler"
241
254
  };
242
255
  await fs.writeFile(path.join(functionDir, "function.json"), JSON.stringify(functionJson, null, 2));
243
- console.log("Azure Functions metadata created");
244
- console.log("\nInstalling minimal runtime dependencies...");
256
+ console.log(` ${greenCheck()} Azure Functions metadata created
257
+ `);
258
+ console.log("Installing minimal runtime dependencies...");
245
259
  try {
246
260
  const originalPackageJson = JSON.parse(await fs.readFile(path.join(functionsDir, "package.json"), "utf-8"));
247
261
  const minimalPackageJson = {
@@ -260,7 +274,8 @@ async function prepareFunctions() {
260
274
  await execAsync$2("npm install --production --no-package-lock --loglevel=error", {
261
275
  cwd: functionsDir
262
276
  });
263
- console.log("Runtime dependencies installed\n");
277
+ console.log(` ${greenCheck()} Runtime dependencies installed
278
+ `);
264
279
  } catch (error) {
265
280
  console.error("Failed to install dependencies:", error.message);
266
281
  throw error;
@@ -313,12 +328,14 @@ export default {
313
328
  if (fs$1.existsSync(openNextPath)) {
314
329
  console.log("Cleaning previous build output...");
315
330
  fs$1.rmSync(openNextPath, { recursive: true, force: true });
316
- console.log("Previous build cleaned\n");
331
+ console.log(` ${greenCheck()} Previous build cleaned
332
+ `);
317
333
  }
318
334
  console.log("Running OpenNext build...");
319
335
  const externals = ["@opennextjs/aws"].join(",");
320
336
  await build$1(resolvedConfigPath, externals);
321
- console.log("OpenNext build complete\n");
337
+ console.log(` ${greenCheck()} OpenNext build complete
338
+ `);
322
339
  await prepareFunctions();
323
340
  console.log("Build completed successfully!");
324
341
  console.log("\nOutput: .open-next/");
@@ -378,19 +395,19 @@ async function deploy$1(options) {
378
395
  environment,
379
396
  applicationInsights: options.applicationInsights ?? false
380
397
  });
381
- console.log(`${colors.green}\u2713${colors.reset} Infrastructure ready
398
+ console.log(` ${greenCheck()} Infrastructure ready
382
399
  `);
383
400
  } else {
384
401
  console.log("Skipping infrastructure provisioning\n");
385
402
  }
386
403
  console.log("Uploading static assets...");
387
404
  await uploadStaticAssets(appName, resourceGroup);
388
- console.log(`${colors.green}\u2713${colors.reset} Assets uploaded
405
+ console.log(` ${greenCheck()} Assets uploaded
389
406
  `);
390
407
  console.log("Deploying Function App...");
391
408
  const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
392
409
  await deployFunctionApp(functionAppName, resourceGroup);
393
- console.log(`${colors.green}\u2713${colors.reset} Function App deployed
410
+ console.log(` ${greenCheck()} Function App deployed
394
411
  `);
395
412
  await performPostflightChecks(
396
413
  resourceGroup,
@@ -401,7 +418,7 @@ async function deploy$1(options) {
401
418
  );
402
419
  } catch (error) {
403
420
  console.error(`
404
- ${colors.red}\u2717${colors.reset} Deployment failed: ${error.message}`);
421
+ ${redX()} Deployment failed: ${error.message}`);
405
422
  process.exit(1);
406
423
  }
407
424
  }
@@ -425,16 +442,17 @@ async function checkRequiredProviders(applicationInsights) {
425
442
  if (applicationInsights) {
426
443
  requiredProviders.push("Microsoft.AlertsManagement");
427
444
  }
428
- console.log("Checking Azure resource providers...");
445
+ console.log("Checking Azure resource providers...\n");
429
446
  for (const provider of requiredProviders) {
430
447
  const { stdout } = await execAsync$1(
431
448
  `az provider show --namespace ${provider} --query "registrationState" -o tsv`
432
449
  );
433
450
  const state = stdout.trim();
434
451
  if (state !== "Registered") {
435
- console.log(` Registering ${provider}...`);
452
+ console.log(`Registering ${provider}...`);
436
453
  await execAsync$1(`az provider register --namespace ${provider} --wait`);
437
- console.log(` ${colors.green}\u2713${colors.reset} ${provider} registered`);
454
+ console.log(` ${greenCheck()} ${provider} registered
455
+ `);
438
456
  }
439
457
  }
440
458
  }
@@ -459,10 +477,8 @@ async function checkQuotaAvailability(location, environment) {
459
477
  };
460
478
  const requiredSku = skuMap[environment];
461
479
  if (requiredSku.quota === 0) {
462
- console.error(
463
- `
464
- ${colors.red}\u2717${colors.reset} Quota Error: No quota available for ${environment} environment`
465
- );
480
+ console.error(`
481
+ ${redX()} Quota Error: No quota available for ${environment} environment`);
466
482
  console.error(` Required: ${requiredSku.name}`);
467
483
  console.error(` Current Limit: ${requiredSku.quota}
468
484
  `);
@@ -486,13 +502,16 @@ ${colors.red}\u2717${colors.reset} Quota Error: No quota available for ${environ
486
502
  }
487
503
  throw new Error(`No ${requiredSku.type} quota available for ${environment} environment in ${location}`);
488
504
  }
489
- console.log(` ${colors.green}\u2713${colors.reset} ${requiredSku.name}: ${requiredSku.quota} instances available`);
505
+ console.log(` ${greenCheck()} ${requiredSku.name}: ${requiredSku.quota} instances available
506
+ `);
490
507
  if (environment === "dev" && ep1Limit > 0) {
491
508
  console.log(
492
- ` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance`
509
+ ` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance
510
+ `
493
511
  );
494
512
  } else if (environment !== "dev" && y1Limit > 0) {
495
- console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost`);
513
+ console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost
514
+ `);
496
515
  }
497
516
  } catch (error) {
498
517
  if (error.message?.includes("No") && error.message?.includes("quota available")) {
@@ -511,7 +530,8 @@ async function checkAzureSubscriptionPermissions() {
511
530
  if (account.State !== "Enabled") {
512
531
  throw new Error(`Subscription "${account.Name}" is not enabled (state: ${account.State})`);
513
532
  }
514
- console.log(` ${colors.green}\u2713${colors.reset} Subscription: ${account.Name} (${account.State})`);
533
+ console.log(` ${greenCheck()} Subscription: ${account.Name} (${account.State})
534
+ `);
515
535
  } catch (error) {
516
536
  throw new Error(`Failed to verify subscription permissions: ${error.message}`);
517
537
  }
@@ -532,13 +552,15 @@ async function checkLocation(location) {
532
552
  Available regions: ${available.trim().split("\n").slice(0, 10).join(", ")}`
533
553
  );
534
554
  }
535
- console.log(` ${colors.green}\u2713${colors.reset} Region: ${locations[0].DisplayName} (${location})`);
555
+ console.log(` ${greenCheck()} Region: ${locations[0].DisplayName} (${location})
556
+ `);
536
557
  } catch (error) {
537
558
  if (error.message.includes("Invalid location")) {
538
559
  throw error;
539
560
  }
540
561
  console.warn(
541
- ` ${colors.yellow}Warning:${colors.reset} Could not validate location (continuing anyway): ${error.message}`
562
+ ` ${colors.yellow}Warning:${colors.reset} Could not validate location (continuing anyway): ${error.message}
563
+ `
542
564
  );
543
565
  }
544
566
  }
@@ -562,7 +584,8 @@ Run 'opennextjs-azure build' to regenerate`
562
584
  );
563
585
  }
564
586
  }
565
- console.log(` ${colors.green}\u2713${colors.reset} Build output structure valid`);
587
+ console.log(` ${greenCheck()} Build output structure valid
588
+ `);
566
589
  }
567
590
  async function checkExistingInfrastructure(appName, resourceGroup, environment) {
568
591
  console.log("Checking existing infrastructure...");
@@ -593,10 +616,10 @@ async function checkExistingInfrastructure(appName, resourceGroup, environment)
593
616
  "Cannot skip infrastructure provisioning - required resources missing:\n" + errors.map((e) => ` \u2022 ${e}`).join("\n") + "\n\nRun without --skip-infrastructure to provision resources first."
594
617
  );
595
618
  }
596
- console.log(` ${colors.green}\u2713${colors.reset} All required infrastructure exists`);
619
+ console.log(` ${greenCheck()} All required infrastructure exists`);
597
620
  }
598
621
  async function performPostflightChecks(resourceGroup, functionAppName, location, environment, applicationInsights) {
599
- console.log("Performing post-deployment verification...\n");
622
+ console.log("Performing post-deployment verification...");
600
623
  try {
601
624
  const { stdout: subIdStdout } = await execAsync$1("az account show --query id -o tsv");
602
625
  const subscriptionId = subIdStdout.trim();
@@ -615,8 +638,8 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
615
638
  const functionUrl = `https://${funcApp.DefaultHostName}`;
616
639
  const assetsUrl = `https://${storage.Name}.blob.core.windows.net/assets`;
617
640
  const portalUrl = `https://portal.azure.com/#@/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}`;
618
- 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");
619
- console.log(`${colors.green}\u2713${colors.reset} Deployment Complete!`);
641
+ 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");
642
+ console.log(`${greenCheck()} Deployment Complete!`);
620
643
  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");
621
644
  console.log("\nApplication:");
622
645
  console.log(` App URL: ${functionUrl}`);
@@ -658,7 +681,7 @@ async function performPostflightChecks(resourceGroup, functionAppName, location,
658
681
  `${colors.yellow}Warning:${colors.reset} Could not retrieve all deployment details: ${error.message}`
659
682
  );
660
683
  console.log(`
661
- ${colors.green}\u2713${colors.reset} Deployment completed, but some post-flight checks failed.`);
684
+ ${greenCheck()} Deployment completed, but some post-flight checks failed.`);
662
685
  console.log(` View resources: az resource list --resource-group ${resourceGroup} -o table
663
686
  `);
664
687
  }
@@ -832,4 +855,4 @@ async function getResourceGroupLocation(resourceGroup) {
832
855
  }
833
856
  }
834
857
 
835
- export { build as b, deploy as d, init as i };
858
+ export { build as b, deploy as d, greenCheck as g, init as i, promptForInput as p, redX as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opennextjs-azure",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Azure adapter for Next.js applications using OpenNext",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",