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 +16 -1
- package/dist/adapters/wrappers/azure-functions.js +5 -2
- package/dist/cli/index.js +453 -5
- package/dist/deploy.js +52 -29
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/opennextjs-azure)
|
|
6
6
|
[](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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
91
|
-
|
|
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.
|
|
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(
|
|
244
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
${
|
|
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(`
|
|
452
|
+
console.log(`Registering ${provider}...`);
|
|
436
453
|
await execAsync$1(`az provider register --namespace ${provider} --wait`);
|
|
437
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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
|
|
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(`${
|
|
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
|
-
${
|
|
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 };
|