opennextjs-azure 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +194 -0
  3. package/dist/adapters/converters/azure-http.d.mts +22 -0
  4. package/dist/adapters/converters/azure-http.d.ts +22 -0
  5. package/dist/adapters/converters/azure-http.js +97 -0
  6. package/dist/adapters/wrappers/azure-functions.d.mts +10 -0
  7. package/dist/adapters/wrappers/azure-functions.d.ts +10 -0
  8. package/dist/adapters/wrappers/azure-functions.js +102 -0
  9. package/dist/cli/index.d.mts +2 -0
  10. package/dist/cli/index.d.ts +2 -0
  11. package/dist/cli/index.js +67 -0
  12. package/dist/config/index.d.mts +3 -0
  13. package/dist/config/index.d.ts +3 -0
  14. package/dist/config/index.js +68 -0
  15. package/dist/deploy.js +835 -0
  16. package/dist/index.d.mts +35 -0
  17. package/dist/index.d.ts +35 -0
  18. package/dist/index.js +20 -0
  19. package/dist/infrastructure/main.bicep +241 -0
  20. package/dist/overrides/incrementalCache/azure-blob.d.mts +23 -0
  21. package/dist/overrides/incrementalCache/azure-blob.d.ts +23 -0
  22. package/dist/overrides/incrementalCache/azure-blob.js +89 -0
  23. package/dist/overrides/queue/azure-queue.d.mts +19 -0
  24. package/dist/overrides/queue/azure-queue.d.ts +19 -0
  25. package/dist/overrides/queue/azure-queue.js +39 -0
  26. package/dist/overrides/tagCache/azure-table.d.mts +26 -0
  27. package/dist/overrides/tagCache/azure-table.d.ts +26 -0
  28. package/dist/overrides/tagCache/azure-table.js +104 -0
  29. package/dist/shared/opennextjs-azure.d619537c.d.mts +61 -0
  30. package/dist/shared/opennextjs-azure.d619537c.d.ts +61 -0
  31. package/infrastructure/main.bicep +241 -0
  32. package/package.json +99 -0
package/dist/deploy.js ADDED
@@ -0,0 +1,835 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { build as build$1 } from '@opennextjs/aws/build.js';
7
+ import fs$1 from 'node:fs';
8
+ import readline from 'node:readline';
9
+
10
+ const execAsync$3 = promisify(exec);
11
+ async function scaffoldProject(targetDir, options = {}) {
12
+ console.log("Scaffolding new Next.js project...\n");
13
+ const {
14
+ typescript = true,
15
+ tailwind = true,
16
+ eslint = true,
17
+ srcDir = true,
18
+ appRouter = true,
19
+ importAlias = "@/*",
20
+ packageManager = "pnpm"
21
+ } = options;
22
+ const flags = [
23
+ typescript && "--typescript",
24
+ appRouter && "--app",
25
+ tailwind && "--tailwind",
26
+ eslint && "--eslint",
27
+ srcDir && "--src-dir",
28
+ importAlias && `--import-alias "${importAlias}"`,
29
+ packageManager && `--use-${packageManager}`,
30
+ "--yes"
31
+ ].filter(Boolean).join(" ");
32
+ await execAsync$3(`npx create-next-app@15 ${targetDir} ${flags}`, { cwd: path.dirname(targetDir) });
33
+ console.log("Next.js project created\n");
34
+ const packageJsonPath = path.join(targetDir, "package.json");
35
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
36
+ packageJson.dependencies = {
37
+ ...packageJson.dependencies,
38
+ "opennextjs-azure": "latest"
39
+ };
40
+ packageJson.devDependencies = {
41
+ ...packageJson.devDependencies,
42
+ esbuild: "^0.25.11"
43
+ };
44
+ if (packageJson.scripts.dev) {
45
+ packageJson.scripts.dev = packageJson.scripts.dev.replace(" --turbopack", "");
46
+ }
47
+ if (packageJson.scripts.build) {
48
+ packageJson.scripts.build = packageJson.scripts.build.replace(" --turbopack", "");
49
+ }
50
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
51
+ const nextConfigPath = path.join(targetDir, "next.config.ts");
52
+ let nextConfig = await fs.readFile(nextConfigPath, "utf-8");
53
+ nextConfig = nextConfig.replace(
54
+ /const nextConfig: NextConfig = \{/,
55
+ `const nextConfig: NextConfig = {
56
+ output: "standalone",`
57
+ );
58
+ await fs.writeFile(nextConfigPath, nextConfig);
59
+ console.log("Installing dependencies...");
60
+ await fs.unlink(path.join(targetDir, "pnpm-lock.yaml")).catch(() => {
61
+ });
62
+ await execAsync$3("pnpm install", { cwd: targetDir });
63
+ console.log("Dependencies installed\n");
64
+ console.log("Creating open-next.config.ts...");
65
+ const openNextConfig = `// @ts-nocheck
66
+ export default {
67
+ default: {
68
+ override: {
69
+ wrapper: () => import("./node_modules/opennextjs-azure/dist/adapters/wrappers/azure-functions.js").then(m => m.default),
70
+ converter: () => import("./node_modules/opennextjs-azure/dist/adapters/converters/azure-http.js").then(m => m.default),
71
+ incrementalCache: () => import("./node_modules/opennextjs-azure/dist/overrides/incrementalCache/azure-blob.js").then(m => new m.default()),
72
+ tagCache: () => import("./node_modules/opennextjs-azure/dist/overrides/tagCache/azure-table.js").then(m => new m.default()),
73
+ queue: () => import("./node_modules/opennextjs-azure/dist/overrides/queue/azure-queue.js").then(m => new m.default()),
74
+ proxyExternalRequest: "fetch",
75
+ },
76
+ routePreloadingBehavior: "none",
77
+ },
78
+ middleware: {
79
+ external: false,
80
+ },
81
+ buildOutputPath: ".",
82
+ appPath: ".",
83
+ };
84
+ `;
85
+ await fs.writeFile(path.join(targetDir, "open-next.config.ts"), openNextConfig);
86
+ console.log("Created open-next.config.ts\n");
87
+ }
88
+
89
+ async function init(options) {
90
+ console.log("\u{1F680} Initializing OpenNext Azure project...\n");
91
+ const cwd = process.cwd();
92
+ const files = await fs.readdir(cwd);
93
+ const isEmpty = files.length === 0 || files.length === 1 && files[0] === ".git";
94
+ if (isEmpty || options?.scaffold) {
95
+ console.log("\u{1F4C2} Empty directory detected.\n");
96
+ const answer = await promptUser("Create new Next.js project with opinionated setup? (Y/n): ");
97
+ if (answer.toLowerCase() !== "n") {
98
+ await scaffoldProject(cwd, options);
99
+ }
100
+ }
101
+ const infraDir = path.join(cwd, "infrastructure");
102
+ try {
103
+ try {
104
+ await fs.access(infraDir);
105
+ console.log("infrastructure/ directory already exists");
106
+ const answer = await promptUser("Overwrite? (y/N): ");
107
+ if (answer.toLowerCase() !== "y") {
108
+ console.log("Cancelled.");
109
+ return;
110
+ }
111
+ } catch {
112
+ }
113
+ await fs.mkdir(infraDir, { recursive: true });
114
+ const bicepContent = await getBicepTemplate();
115
+ await fs.writeFile(path.join(infraDir, "main.bicep"), bicepContent);
116
+ const configContent = getAzureConfigTemplate();
117
+ await fs.writeFile(path.join(cwd, "azure.config.json"), configContent);
118
+ try {
119
+ const gitignorePath = path.join(cwd, ".gitignore");
120
+ let gitignore = await fs.readFile(gitignorePath, "utf-8");
121
+ if (!gitignore.includes("azure.config.json")) {
122
+ gitignore += "\n# Azure deployment config (contains resource names)\nazure.config.json\n";
123
+ await fs.writeFile(gitignorePath, gitignore);
124
+ }
125
+ } catch {
126
+ }
127
+ console.log("Created infrastructure/main.bicep");
128
+ console.log("Created azure.config.json");
129
+ console.log("\nNext steps:");
130
+ console.log("1. Edit azure.config.json with your app details");
131
+ console.log("2. Optionally customize infrastructure/main.bicep");
132
+ console.log("3. Run: opennextjs-azure build");
133
+ console.log("4. Run: opennextjs-azure deploy\n");
134
+ } catch (error) {
135
+ console.error("Initialization failed:", error.message);
136
+ process.exit(1);
137
+ }
138
+ }
139
+ async function promptUser(question) {
140
+ const readline = await import('node:readline');
141
+ const rl = readline.createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout
144
+ });
145
+ return new Promise((resolve) => {
146
+ rl.question(question, (answer) => {
147
+ rl.close();
148
+ resolve(answer);
149
+ });
150
+ });
151
+ }
152
+ async function getBicepTemplate() {
153
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
154
+ const templatePath = path.join(currentDir, "../infrastructure/main.bicep");
155
+ return await fs.readFile(templatePath, "utf-8");
156
+ }
157
+ function getAzureConfigTemplate() {
158
+ return `{
159
+ "$schema": "./node_modules/opennextjs-azure/azure.config.schema.json",
160
+ "appName": "my-nextjs-app",
161
+ "resourceGroup": "my-nextjs-app-rg",
162
+ "location": "eastus",
163
+ "environment": "dev",
164
+ "applicationInsights": true
165
+ }
166
+ `;
167
+ }
168
+
169
+ const execAsync$2 = promisify(exec);
170
+ async function prepareFunctions() {
171
+ const functionsDir = path.join(process.cwd(), ".open-next/server-functions/default");
172
+ try {
173
+ await fs.access(functionsDir);
174
+ } catch {
175
+ throw new Error(".open-next/server-functions/default not found. Run 'opennextjs-azure build' first.");
176
+ }
177
+ console.log("Preparing Azure Functions metadata...");
178
+ const hostJson = {
179
+ version: "2.0",
180
+ logging: {
181
+ applicationInsights: {
182
+ samplingSettings: {
183
+ isEnabled: true,
184
+ maxTelemetryItemsPerSecond: 5
185
+ }
186
+ }
187
+ },
188
+ extensionBundle: {
189
+ id: "Microsoft.Azure.Functions.ExtensionBundle",
190
+ version: "[4.*, 5.0.0)"
191
+ },
192
+ extensions: {
193
+ http: {
194
+ routePrefix: ""
195
+ }
196
+ }
197
+ };
198
+ await fs.writeFile(path.join(functionsDir, "host.json"), JSON.stringify(hostJson, null, 2));
199
+ const rootDir = path.join(functionsDir, "root");
200
+ await fs.mkdir(rootDir, { recursive: true });
201
+ const rootFunctionJson = {
202
+ bindings: [
203
+ {
204
+ authLevel: "anonymous",
205
+ type: "httpTrigger",
206
+ direction: "in",
207
+ name: "req",
208
+ methods: ["get", "post", "put", "delete", "patch", "head", "options"],
209
+ route: ""
210
+ },
211
+ {
212
+ type: "http",
213
+ direction: "out",
214
+ name: "res"
215
+ }
216
+ ],
217
+ scriptFile: "../index.mjs",
218
+ entryPoint: "handler"
219
+ };
220
+ await fs.writeFile(path.join(rootDir, "function.json"), JSON.stringify(rootFunctionJson, null, 2));
221
+ const functionDir = path.join(functionsDir, "server");
222
+ await fs.mkdir(functionDir, { recursive: true });
223
+ const functionJson = {
224
+ bindings: [
225
+ {
226
+ authLevel: "anonymous",
227
+ type: "httpTrigger",
228
+ direction: "in",
229
+ name: "req",
230
+ methods: ["get", "post", "put", "delete", "patch", "head", "options"],
231
+ route: "{*path}"
232
+ },
233
+ {
234
+ type: "http",
235
+ direction: "out",
236
+ name: "res"
237
+ }
238
+ ],
239
+ scriptFile: "../index.mjs",
240
+ entryPoint: "handler"
241
+ };
242
+ 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...");
245
+ try {
246
+ const originalPackageJson = JSON.parse(await fs.readFile(path.join(functionsDir, "package.json"), "utf-8"));
247
+ const minimalPackageJson = {
248
+ name: originalPackageJson.name || "nextjs-app",
249
+ version: originalPackageJson.version || "1.0.0",
250
+ private: true,
251
+ dependencies: {
252
+ next: originalPackageJson.dependencies?.next || "latest",
253
+ react: originalPackageJson.dependencies?.react || "latest",
254
+ "react-dom": originalPackageJson.dependencies?.["react-dom"] || "latest"
255
+ }
256
+ };
257
+ await fs.writeFile(path.join(functionsDir, "package.json"), JSON.stringify(minimalPackageJson, null, 2));
258
+ const nodeModulesPath = path.join(functionsDir, "node_modules");
259
+ await fs.rm(nodeModulesPath, { recursive: true, force: true });
260
+ await execAsync$2("npm install --production --no-package-lock --loglevel=error", {
261
+ cwd: functionsDir
262
+ });
263
+ console.log("Runtime dependencies installed\n");
264
+ } catch (error) {
265
+ console.error("Failed to install dependencies:", error.message);
266
+ throw error;
267
+ }
268
+ }
269
+
270
+ async function build(configPath) {
271
+ console.log("Building Next.js app for Azure...\n");
272
+ const baseDir = process.cwd();
273
+ const userConfigPath = configPath || "open-next.config.ts";
274
+ const absoluteUserConfigPath = path.join(baseDir, userConfigPath);
275
+ let resolvedConfigPath = userConfigPath;
276
+ let tempConfigPath = null;
277
+ if (!fs$1.existsSync(absoluteUserConfigPath)) {
278
+ console.log("No open-next.config.ts found, using default Azure configuration\n");
279
+ const { createRequire } = await import('node:module');
280
+ const require = createRequire(import.meta.url);
281
+ const packagePath = path.dirname(require.resolve("opennextjs-azure/package.json"));
282
+ const wrapperPath = path.join(packagePath, "dist/adapters/wrappers/azure-functions.js");
283
+ const converterPath = path.join(packagePath, "dist/adapters/converters/azure-http.js");
284
+ const incrementalCachePath = path.join(packagePath, "dist/overrides/incrementalCache/azure-blob.js");
285
+ const tagCachePath = path.join(packagePath, "dist/overrides/tagCache/azure-table.js");
286
+ const queuePath = path.join(packagePath, "dist/overrides/queue/azure-queue.js");
287
+ tempConfigPath = path.join(baseDir, "open-next.config.ts");
288
+ const configContent = `// @ts-nocheck
289
+ export default {
290
+ default: {
291
+ override: {
292
+ wrapper: () => import("${wrapperPath}").then(m => m.default),
293
+ converter: () => import("${converterPath}").then(m => m.default),
294
+ incrementalCache: () => import("${incrementalCachePath}").then(m => new m.default()),
295
+ tagCache: () => import("${tagCachePath}").then(m => new m.default()),
296
+ queue: () => import("${queuePath}").then(m => new m.default()),
297
+ proxyExternalRequest: "fetch",
298
+ },
299
+ routePreloadingBehavior: "none",
300
+ },
301
+ middleware: {
302
+ external: false,
303
+ },
304
+ buildOutputPath: ".",
305
+ appPath: ".",
306
+ };
307
+ `;
308
+ fs$1.writeFileSync(tempConfigPath, configContent);
309
+ resolvedConfigPath = "open-next.config.ts";
310
+ }
311
+ try {
312
+ const openNextPath = path.join(baseDir, ".open-next");
313
+ if (fs$1.existsSync(openNextPath)) {
314
+ console.log("Cleaning previous build output...");
315
+ fs$1.rmSync(openNextPath, { recursive: true, force: true });
316
+ console.log("Previous build cleaned\n");
317
+ }
318
+ console.log("Running OpenNext build...");
319
+ const externals = ["@opennextjs/aws"].join(",");
320
+ await build$1(resolvedConfigPath, externals);
321
+ console.log("OpenNext build complete\n");
322
+ await prepareFunctions();
323
+ console.log("Build completed successfully!");
324
+ console.log("\nOutput: .open-next/");
325
+ console.log(" \u251C\u2500\u2500 server-functions/default (Azure Functions app)");
326
+ console.log(" \u2514\u2500\u2500 assets (Static files)\n");
327
+ console.log("Next: opennextjs-azure deploy\n");
328
+ } catch (error) {
329
+ console.error("Build failed:", error);
330
+ process.exit(1);
331
+ } finally {
332
+ if (tempConfigPath && fs$1.existsSync(tempConfigPath)) {
333
+ fs$1.unlinkSync(tempConfigPath);
334
+ }
335
+ }
336
+ }
337
+
338
+ const execAsync$1 = promisify(exec);
339
+ const colors = {
340
+ green: "\x1B[32m",
341
+ red: "\x1B[31m",
342
+ yellow: "\x1B[33m",
343
+ reset: "\x1B[0m"
344
+ };
345
+ async function deploy$1(options) {
346
+ const {
347
+ appName,
348
+ resourceGroup = `${appName}-rg`,
349
+ location = "eastus",
350
+ environment = "dev",
351
+ skipInfrastructure = false
352
+ } = options;
353
+ console.log(`Deploying ${appName} to Azure (${environment} environment)
354
+ `);
355
+ try {
356
+ await checkAzureCLI();
357
+ await checkAzureLogin();
358
+ await checkAzureSubscriptionPermissions();
359
+ await checkLocation(location);
360
+ await checkRequiredProviders(options.applicationInsights);
361
+ await checkQuotaAvailability(location, environment);
362
+ await checkBuildOutput();
363
+ if (skipInfrastructure) {
364
+ await checkExistingInfrastructure(appName, resourceGroup, environment);
365
+ }
366
+ let deploymentOutputs;
367
+ if (!skipInfrastructure) {
368
+ await syncBicepTemplate();
369
+ console.log("Provisioning Azure infrastructure...");
370
+ console.log(` Resource Group: ${resourceGroup}`);
371
+ console.log(` Location: ${location}`);
372
+ console.log(` Environment: ${environment}
373
+ `);
374
+ deploymentOutputs = await provisionInfrastructure({
375
+ appName,
376
+ resourceGroup,
377
+ location,
378
+ environment,
379
+ applicationInsights: options.applicationInsights ?? false
380
+ });
381
+ console.log(`${colors.green}\u2713${colors.reset} Infrastructure ready
382
+ `);
383
+ } else {
384
+ console.log("Skipping infrastructure provisioning\n");
385
+ }
386
+ console.log("Uploading static assets...");
387
+ await uploadStaticAssets(appName, resourceGroup);
388
+ console.log(`${colors.green}\u2713${colors.reset} Assets uploaded
389
+ `);
390
+ console.log("Deploying Function App...");
391
+ const functionAppName = deploymentOutputs?.functionApp || `${appName}-func-${environment}`;
392
+ await deployFunctionApp(functionAppName, resourceGroup);
393
+ console.log(`${colors.green}\u2713${colors.reset} Function App deployed
394
+ `);
395
+ await performPostflightChecks(
396
+ resourceGroup,
397
+ functionAppName,
398
+ location,
399
+ environment,
400
+ options.applicationInsights
401
+ );
402
+ } catch (error) {
403
+ console.error(`
404
+ ${colors.red}\u2717${colors.reset} Deployment failed: ${error.message}`);
405
+ process.exit(1);
406
+ }
407
+ }
408
+ async function checkAzureCLI() {
409
+ try {
410
+ await execAsync$1("az --version");
411
+ } catch {
412
+ throw new Error("Azure CLI not found. Install it from: https://docs.microsoft.com/cli/azure/install-azure-cli");
413
+ }
414
+ }
415
+ async function checkAzureLogin() {
416
+ try {
417
+ await execAsync$1("az account show");
418
+ } catch {
419
+ console.log("Not logged in to Azure. Running 'az login'...");
420
+ await execAsync$1("az login");
421
+ }
422
+ }
423
+ async function checkRequiredProviders(applicationInsights) {
424
+ const requiredProviders = ["Microsoft.Web", "Microsoft.Storage", "Microsoft.Compute", "Microsoft.Quota"];
425
+ if (applicationInsights) {
426
+ requiredProviders.push("Microsoft.AlertsManagement");
427
+ }
428
+ console.log("Checking Azure resource providers...");
429
+ for (const provider of requiredProviders) {
430
+ const { stdout } = await execAsync$1(
431
+ `az provider show --namespace ${provider} --query "registrationState" -o tsv`
432
+ );
433
+ const state = stdout.trim();
434
+ if (state !== "Registered") {
435
+ console.log(` Registering ${provider}...`);
436
+ await execAsync$1(`az provider register --namespace ${provider} --wait`);
437
+ console.log(` ${colors.green}\u2713${colors.reset} ${provider} registered`);
438
+ }
439
+ }
440
+ }
441
+ async function checkQuotaAvailability(location, environment) {
442
+ console.log("Checking Azure quota availability...");
443
+ try {
444
+ const { stdout: subscriptionId } = await execAsync$1("az account show --query id -o tsv");
445
+ const subId = subscriptionId.trim();
446
+ const { stdout: quotaJson } = await execAsync$1(
447
+ `az rest --method get --url "https://management.azure.com/subscriptions/${subId}/providers/Microsoft.Web/locations/${location}/providers/Microsoft.Quota/quotas?api-version=2023-02-01"`
448
+ );
449
+ const quotaData = JSON.parse(quotaJson);
450
+ const quotas = quotaData.value || [];
451
+ const y1Quota = quotas.find((q) => q.name === "Y1" || q.name?.value === "Y1");
452
+ const ep1Quota = quotas.find((q) => q.name === "EP1" || q.name?.value === "EP1");
453
+ const y1Limit = y1Quota?.properties?.limit?.value || 0;
454
+ const ep1Limit = ep1Quota?.properties?.limit?.value || 0;
455
+ const skuMap = {
456
+ dev: { name: "Y1 (Consumption)", quota: y1Limit, type: "Dynamic" },
457
+ staging: { name: "EP1 (Elastic Premium)", quota: ep1Limit, type: "ElasticPremium" },
458
+ prod: { name: "EP1 (Elastic Premium)", quota: ep1Limit, type: "ElasticPremium" }
459
+ };
460
+ const requiredSku = skuMap[environment];
461
+ if (requiredSku.quota === 0) {
462
+ console.error(
463
+ `
464
+ ${colors.red}\u2717${colors.reset} Quota Error: No quota available for ${environment} environment`
465
+ );
466
+ console.error(` Required: ${requiredSku.name}`);
467
+ console.error(` Current Limit: ${requiredSku.quota}
468
+ `);
469
+ if (y1Limit > 0 && environment !== "dev") {
470
+ console.log(` Suggestion: Deploy to dev environment instead (has quota: ${y1Limit})`);
471
+ console.log(` Command: opennextjs-azure deploy --environment dev
472
+ `);
473
+ } else if (ep1Limit > 0 && environment === "dev") {
474
+ console.log(` Suggestion: Deploy to prod environment instead (has quota: ${ep1Limit})`);
475
+ console.log(` Command: opennextjs-azure deploy --environment prod
476
+ `);
477
+ } else {
478
+ console.log(` To request quota increase:`);
479
+ console.log(
480
+ ` 1. Visit: https://portal.azure.com/#view/Microsoft_Azure_Capacity/QuotaMenuBlade/~/myQuotas`
481
+ );
482
+ console.log(
483
+ ` 2. Or run: az rest --method put --url "https://management.azure.com/subscriptions/${subId}/providers/Microsoft.Web/locations/${location}/providers/Microsoft.Quota/quotas/${requiredSku.name.split(" ")[0]}?api-version=2023-02-01" --body '{"properties":{"limit":{"value":10}}}'`
484
+ );
485
+ console.log();
486
+ }
487
+ throw new Error(`No ${requiredSku.type} quota available for ${environment} environment in ${location}`);
488
+ }
489
+ console.log(` ${colors.green}\u2713${colors.reset} ${requiredSku.name}: ${requiredSku.quota} instances available`);
490
+ if (environment === "dev" && ep1Limit > 0) {
491
+ console.log(
492
+ ` Premium tier also available (${ep1Limit} instances) - use --environment prod for better performance`
493
+ );
494
+ } else if (environment !== "dev" && y1Limit > 0) {
495
+ console.log(` Consumption tier available (${y1Limit} instances) - use --environment dev for lower cost`);
496
+ }
497
+ } catch (error) {
498
+ if (error.message?.includes("No") && error.message?.includes("quota available")) {
499
+ throw error;
500
+ }
501
+ console.warn(
502
+ ` ${colors.yellow}Warning:${colors.reset} Could not verify quota (continuing anyway): ${error.message}`
503
+ );
504
+ }
505
+ }
506
+ async function checkAzureSubscriptionPermissions() {
507
+ console.log("Checking Azure subscription permissions...");
508
+ try {
509
+ const { stdout } = await execAsync$1("az account show --query '{Name:name, Id:id, State:state}' -o json");
510
+ const account = JSON.parse(stdout);
511
+ if (account.State !== "Enabled") {
512
+ throw new Error(`Subscription "${account.Name}" is not enabled (state: ${account.State})`);
513
+ }
514
+ console.log(` ${colors.green}\u2713${colors.reset} Subscription: ${account.Name} (${account.State})`);
515
+ } catch (error) {
516
+ throw new Error(`Failed to verify subscription permissions: ${error.message}`);
517
+ }
518
+ }
519
+ async function checkLocation(location) {
520
+ console.log("Validating Azure region...");
521
+ try {
522
+ const { stdout } = await execAsync$1(
523
+ `az account list-locations --query "[?name=='${location}'].{Name:name, DisplayName:displayName}" -o json`
524
+ );
525
+ const locations = JSON.parse(stdout);
526
+ if (locations.length === 0) {
527
+ const { stdout: available } = await execAsync$1(
528
+ `az account list-locations --query "[?metadata.regionCategory=='Recommended'].name" -o tsv`
529
+ );
530
+ throw new Error(
531
+ `Invalid location: ${location}
532
+ Available regions: ${available.trim().split("\n").slice(0, 10).join(", ")}`
533
+ );
534
+ }
535
+ console.log(` ${colors.green}\u2713${colors.reset} Region: ${locations[0].DisplayName} (${location})`);
536
+ } catch (error) {
537
+ if (error.message.includes("Invalid location")) {
538
+ throw error;
539
+ }
540
+ console.warn(
541
+ ` ${colors.yellow}Warning:${colors.reset} Could not validate location (continuing anyway): ${error.message}`
542
+ );
543
+ }
544
+ }
545
+ async function checkBuildOutput() {
546
+ console.log("Validating build output...");
547
+ const openNextPath = path.join(process.cwd(), ".open-next");
548
+ const requiredPaths = ["assets", "server-functions/default"];
549
+ try {
550
+ await fs.access(openNextPath);
551
+ } catch {
552
+ throw new Error("Build not found. Run 'opennextjs-azure build' first\nExpected directory: .open-next/");
553
+ }
554
+ for (const requiredPath of requiredPaths) {
555
+ const fullPath = path.join(openNextPath, requiredPath);
556
+ try {
557
+ await fs.access(fullPath);
558
+ } catch {
559
+ throw new Error(
560
+ `Invalid build output: Missing ${requiredPath}
561
+ Run 'opennextjs-azure build' to regenerate`
562
+ );
563
+ }
564
+ }
565
+ console.log(` ${colors.green}\u2713${colors.reset} Build output structure valid`);
566
+ }
567
+ async function checkExistingInfrastructure(appName, resourceGroup, environment) {
568
+ console.log("Checking existing infrastructure...");
569
+ const functionAppName = `${appName}-func-${environment}`;
570
+ const errors = [];
571
+ try {
572
+ const { stdout: rgExists } = await execAsync$1(`az group exists --name ${resourceGroup}`);
573
+ if (rgExists.trim() !== "true") {
574
+ errors.push(`Resource group "${resourceGroup}" does not exist`);
575
+ }
576
+ } catch {
577
+ errors.push(`Resource group "${resourceGroup}" does not exist`);
578
+ }
579
+ try {
580
+ await execAsync$1(`az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`);
581
+ } catch {
582
+ errors.push("Storage account not found in resource group");
583
+ }
584
+ try {
585
+ await execAsync$1(
586
+ `az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query "name" -o tsv`
587
+ );
588
+ } catch {
589
+ errors.push(`Function app "${functionAppName}" does not exist`);
590
+ }
591
+ if (errors.length > 0) {
592
+ throw new Error(
593
+ "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
+ );
595
+ }
596
+ console.log(` ${colors.green}\u2713${colors.reset} All required infrastructure exists`);
597
+ }
598
+ async function performPostflightChecks(resourceGroup, functionAppName, location, environment, applicationInsights) {
599
+ console.log("Performing post-deployment verification...\n");
600
+ try {
601
+ const { stdout: subIdStdout } = await execAsync$1("az account show --query id -o tsv");
602
+ const subscriptionId = subIdStdout.trim();
603
+ const { stdout: funcStdout } = await execAsync$1(
604
+ `az functionapp show --resource-group ${resourceGroup} --name ${functionAppName} --query "{State:state, DefaultHostName:defaultHostName, Kind:kind, OutboundIpAddresses:outboundIpAddresses}" -o json`
605
+ );
606
+ const funcApp = JSON.parse(funcStdout);
607
+ const { stdout: storageStdout } = await execAsync$1(
608
+ `az storage account list --resource-group ${resourceGroup} --query "[0].{Name:name, Location:location, Sku:sku.name, Kind:kind}" -o json`
609
+ );
610
+ const storage = JSON.parse(storageStdout);
611
+ const { stdout: planStdout } = await execAsync$1(
612
+ `az appservice plan show --resource-group ${resourceGroup} --name ${functionAppName.replace("-func-", "-plan-")} --query "{Sku:sku.name, Tier:sku.tier, Capacity:sku.capacity}" -o json`
613
+ );
614
+ const plan = JSON.parse(planStdout);
615
+ const functionUrl = `https://${funcApp.DefaultHostName}`;
616
+ const assetsUrl = `https://${storage.Name}.blob.core.windows.net/assets`;
617
+ 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!`);
620
+ 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
+ console.log("\nApplication:");
622
+ console.log(` App URL: ${functionUrl}`);
623
+ console.log(` Assets URL: ${assetsUrl}`);
624
+ console.log(` Status: ${funcApp.State}`);
625
+ console.log(` Type: ${funcApp.Kind}`);
626
+ console.log("\nInfrastructure:");
627
+ console.log(` Resource Group: ${resourceGroup}`);
628
+ console.log(` Region: ${location}`);
629
+ console.log(` Environment: ${environment}`);
630
+ console.log("\nConfiguration:");
631
+ console.log(` App Service Plan: ${plan.Tier} (${plan.Sku})`);
632
+ console.log(` Storage Account: ${storage.Name} (${storage.Sku})`);
633
+ console.log(` Capacity: ${plan.Capacity || 1} instance(s)`);
634
+ if (applicationInsights) {
635
+ try {
636
+ const { stdout: insightsStdout } = await execAsync$1(
637
+ `az monitor app-insights component show --app ${functionAppName.replace("-func-", "-insights-")} --resource-group ${resourceGroup} --query "{Name:name, InstrumentationKey:instrumentationKey}" -o json`
638
+ );
639
+ const insights = JSON.parse(insightsStdout);
640
+ console.log(` App Insights: ${insights.Name}`);
641
+ } catch {
642
+ }
643
+ }
644
+ console.log("\nQuick Actions:");
645
+ console.log(
646
+ ` View logs: az functionapp log tail --name ${functionAppName} --resource-group ${resourceGroup}`
647
+ );
648
+ console.log(` Open in portal: ${portalUrl}`);
649
+ 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");
650
+ if (funcApp.State !== "Running") {
651
+ console.warn(
652
+ `${colors.yellow}Warning:${colors.reset} Function App state is "${funcApp.State}" (expected "Running")`
653
+ );
654
+ console.warn(" It may take a few minutes for the app to start.\n");
655
+ }
656
+ } catch (error) {
657
+ console.warn(
658
+ `${colors.yellow}Warning:${colors.reset} Could not retrieve all deployment details: ${error.message}`
659
+ );
660
+ console.log(`
661
+ ${colors.green}\u2713${colors.reset} Deployment completed, but some post-flight checks failed.`);
662
+ console.log(` View resources: az resource list --resource-group ${resourceGroup} -o table
663
+ `);
664
+ }
665
+ }
666
+ async function syncBicepTemplate() {
667
+ const { fileURLToPath } = await import('node:url');
668
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
669
+ const packageBicepPath = path.join(currentDir, "../infrastructure/main.bicep");
670
+ const projectBicepPath = path.join(process.cwd(), "infrastructure/main.bicep");
671
+ const bicepContent = await fs.readFile(packageBicepPath, "utf-8");
672
+ await fs.mkdir(path.dirname(projectBicepPath), { recursive: true });
673
+ await fs.writeFile(projectBicepPath, bicepContent);
674
+ }
675
+ async function provisionInfrastructure(options) {
676
+ const { appName, resourceGroup, location, environment, applicationInsights } = options;
677
+ await execAsync$1(`az group create --name ${resourceGroup} --location ${location}`);
678
+ const bicepPath = path.join(process.cwd(), "infrastructure/main.bicep");
679
+ const enableAppInsights = applicationInsights ? "true" : "false";
680
+ const { stdout } = await execAsync$1(
681
+ `az deployment group create --resource-group ${resourceGroup} --template-file ${bicepPath} --parameters appName=${appName} environment=${environment} enableApplicationInsights=${enableAppInsights} --query 'properties.outputs.deploymentInfo.value' --output json`
682
+ );
683
+ return JSON.parse(stdout);
684
+ }
685
+ async function uploadStaticAssets(appName, resourceGroup) {
686
+ const assetsPath = path.join(process.cwd(), ".open-next/assets");
687
+ const { stdout } = await execAsync$1(
688
+ `az storage account list --resource-group ${resourceGroup} --query "[0].name" -o tsv`
689
+ );
690
+ const storageAccountName = stdout.trim();
691
+ await execAsync$1(
692
+ `az storage blob upload-batch --account-name ${storageAccountName} --destination assets --source ${assetsPath} --overwrite`
693
+ );
694
+ }
695
+ async function deployFunctionApp(functionAppName, resourceGroup) {
696
+ const functionsPath = path.join(process.cwd(), ".open-next/server-functions/default");
697
+ const zipPath = path.join(process.cwd(), ".open-next/function-app.zip");
698
+ await execAsync$1(`cd ${functionsPath} && zip -r ${zipPath} . -q`, {
699
+ maxBuffer: 100 * 1024 * 1024
700
+ });
701
+ await execAsync$1(
702
+ `az functionapp deployment source config-zip --resource-group ${resourceGroup} --name ${functionAppName} --src ${zipPath}`,
703
+ {
704
+ maxBuffer: 100 * 1024 * 1024
705
+ }
706
+ );
707
+ await fs.unlink(zipPath);
708
+ }
709
+
710
+ const execAsync = promisify(exec);
711
+ async function promptForInput(question) {
712
+ const rl = readline.createInterface({
713
+ input: process.stdin,
714
+ output: process.stdout
715
+ });
716
+ return new Promise((resolve) => {
717
+ rl.question(question, (answer) => {
718
+ rl.close();
719
+ resolve(answer.trim());
720
+ });
721
+ });
722
+ }
723
+ async function selectResourceGroup() {
724
+ console.log("\u{1F50D} Fetching your Azure resource groups...\n");
725
+ try {
726
+ const { stdout } = await execAsync("az group list --query '[].{name:name, location:location}' -o json");
727
+ const groups = JSON.parse(stdout);
728
+ if (groups.length === 0) {
729
+ console.log("No existing resource groups found.\n");
730
+ return await createNewResourceGroup();
731
+ }
732
+ console.log("\u{1F4C1} Existing Resource Groups:");
733
+ groups.forEach((group, index) => {
734
+ console.log(` ${index + 1}. ${group.name} (${group.location})`);
735
+ });
736
+ console.log(` ${groups.length + 1}. Create new resource group
737
+ `);
738
+ const choice = await promptForInput("Select a resource group (number): ");
739
+ const choiceNum = parseInt(choice, 10);
740
+ if (choiceNum >= 1 && choiceNum <= groups.length) {
741
+ const selected = groups[choiceNum - 1];
742
+ console.log(`\u2705 Selected: ${selected.name}
743
+ `);
744
+ return { name: selected.name, isNew: false };
745
+ } else if (choiceNum === groups.length + 1) {
746
+ return await createNewResourceGroup();
747
+ } else {
748
+ console.log("Invalid choice. Please try again.\n");
749
+ return await selectResourceGroup();
750
+ }
751
+ } catch (error) {
752
+ console.error("Failed to fetch resource groups:", error.message);
753
+ throw error;
754
+ }
755
+ }
756
+ async function createNewResourceGroup() {
757
+ const name = await promptForInput("Resource group name: ");
758
+ const location = await promptForInput("Location (e.g., eastus, westus2) [eastus]: ");
759
+ const finalLocation = location || "eastus";
760
+ console.log(`
761
+ \u2728 Will create new resource group: ${name} in ${finalLocation}
762
+ `);
763
+ return {
764
+ name,
765
+ isNew: true,
766
+ location: finalLocation
767
+ };
768
+ }
769
+ async function selectEnvironment() {
770
+ console.log("\n\u{1F30D} Environment:");
771
+ console.log(" 1. dev (Consumption plan, ~$5-20/month)");
772
+ console.log(" 2. staging (Consumption plan)");
773
+ console.log(" 3. prod (Premium plan, always warm, ~$70-150/month)\n");
774
+ const choice = await promptForInput("Select environment [1]: ");
775
+ const envMap = {
776
+ "1": "dev",
777
+ "2": "staging",
778
+ "3": "prod"
779
+ };
780
+ return envMap[choice] || "dev";
781
+ }
782
+
783
+ async function deploy(options) {
784
+ const cwd = process.cwd();
785
+ const configPath = path.join(cwd, "azure.config.json");
786
+ let config = {};
787
+ try {
788
+ const configContent = await fs.readFile(configPath, "utf-8");
789
+ config = JSON.parse(configContent);
790
+ } catch {
791
+ console.warn("\u26A0\uFE0F azure.config.json not found.");
792
+ console.warn(" Run 'opennextjs-azure init' to create project structure.\n");
793
+ }
794
+ const bicepPath = path.join(cwd, "infrastructure/main.bicep");
795
+ try {
796
+ await fs.access(bicepPath);
797
+ } catch {
798
+ console.error("\u274C infrastructure/main.bicep not found!");
799
+ console.error(" Run 'opennextjs-azure init' to create it.\n");
800
+ process.exit(1);
801
+ }
802
+ let resourceGroup = options.resourceGroup || config.resourceGroup;
803
+ let resourceGroupLocation;
804
+ if (!resourceGroup) {
805
+ const result = await selectResourceGroup();
806
+ resourceGroup = result.name;
807
+ if (result.isNew) {
808
+ resourceGroupLocation = result.location;
809
+ } else {
810
+ resourceGroupLocation = await getResourceGroupLocation(resourceGroup);
811
+ }
812
+ }
813
+ const environment = options.environment || config.environment || await selectEnvironment();
814
+ await deploy$1({
815
+ appName: options.appName || config.appName || path.basename(cwd),
816
+ resourceGroup,
817
+ location: resourceGroupLocation || options.location || config.location || "eastus",
818
+ environment,
819
+ skipInfrastructure: options.skipInfrastructure,
820
+ applicationInsights: config.applicationInsights ?? false
821
+ });
822
+ }
823
+ async function getResourceGroupLocation(resourceGroup) {
824
+ const { exec } = await import('node:child_process');
825
+ const { promisify } = await import('node:util');
826
+ const execAsync = promisify(exec);
827
+ try {
828
+ const { stdout } = await execAsync(`az group show --name ${resourceGroup} --query location -o tsv`);
829
+ return stdout.trim();
830
+ } catch {
831
+ return "eastus";
832
+ }
833
+ }
834
+
835
+ export { build as b, deploy as d, init as i };