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
@@ -0,0 +1,104 @@
1
+ import { TableClient, AzureNamedKeyCredential } from '@azure/data-tables';
2
+ import { getAzureConfig } from '../../config/index.js';
3
+
4
+ class AzureTableTagCache {
5
+ mode = "original";
6
+ name = "azure-table";
7
+ tableClient;
8
+ constructor() {
9
+ const { storage } = getAzureConfig();
10
+ const connectionString = storage.connectionString;
11
+ const accountName = storage.accountName;
12
+ const accountKey = storage.accountKey;
13
+ const tableName = storage.tableName || "nextjstags";
14
+ if (connectionString) {
15
+ this.tableClient = TableClient.fromConnectionString(connectionString, tableName);
16
+ } else if (accountName && accountKey) {
17
+ const credential = new AzureNamedKeyCredential(accountName, accountKey);
18
+ this.tableClient = new TableClient(`https://${accountName}.table.core.windows.net`, tableName, credential);
19
+ }
20
+ }
21
+ buildKey(key) {
22
+ const { NEXT_BUILD_ID } = process.env;
23
+ return `${NEXT_BUILD_ID}/${key}`;
24
+ }
25
+ async getByTag(tag) {
26
+ try {
27
+ const queryKey = this.buildKey(tag);
28
+ const entities = this.tableClient.listEntities({
29
+ queryOptions: { filter: `PartitionKey eq '${queryKey}'` }
30
+ });
31
+ const paths = [];
32
+ for await (const entity of entities) {
33
+ if (entity.rowKey) {
34
+ const { NEXT_BUILD_ID } = process.env;
35
+ const path = entity.rowKey.toString().replace(`${NEXT_BUILD_ID}/`, "");
36
+ paths.push(path);
37
+ }
38
+ }
39
+ return paths;
40
+ } catch (error) {
41
+ process.stderr.write(`Failed to get by tag from Azure Table: ${error}
42
+ `);
43
+ return [];
44
+ }
45
+ }
46
+ async getByPath(path) {
47
+ try {
48
+ const queryKey = this.buildKey(path);
49
+ const entities = this.tableClient.listEntities({
50
+ queryOptions: { filter: `RowKey eq '${queryKey}'` }
51
+ });
52
+ const tags = [];
53
+ for await (const entity of entities) {
54
+ if (entity.partitionKey) {
55
+ const { NEXT_BUILD_ID: buildId } = process.env;
56
+ const tag = entity.partitionKey.toString().replace(`${buildId}/`, "");
57
+ tags.push(tag);
58
+ }
59
+ }
60
+ return tags;
61
+ } catch (error) {
62
+ process.stderr.write(`Failed to get by path from Azure Table: ${error}
63
+ `);
64
+ return [];
65
+ }
66
+ }
67
+ async getLastModified(path, lastModified) {
68
+ try {
69
+ const queryKey = this.buildKey(path);
70
+ const entities = this.tableClient.listEntities({
71
+ queryOptions: {
72
+ filter: `RowKey eq '${queryKey}' and RevalidatedAt gt ${lastModified ?? 0}L`
73
+ }
74
+ });
75
+ for await (const entity of entities) {
76
+ if (entity.revalidatedAt) {
77
+ return -1;
78
+ }
79
+ }
80
+ return lastModified ?? Date.now();
81
+ } catch (error) {
82
+ process.stderr.write(`Failed to get last modified from Azure Table: ${error}
83
+ `);
84
+ return lastModified ?? Date.now();
85
+ }
86
+ }
87
+ async writeTags(tags) {
88
+ try {
89
+ for (const { tag, path, revalidatedAt } of tags) {
90
+ const entity = {
91
+ partitionKey: this.buildKey(tag),
92
+ rowKey: this.buildKey(path),
93
+ revalidatedAt: revalidatedAt ?? Date.now()
94
+ };
95
+ await this.tableClient.upsertEntity(entity, "Merge");
96
+ }
97
+ } catch (error) {
98
+ process.stderr.write(`Failed to write tags to Azure Table: ${error}
99
+ `);
100
+ }
101
+ }
102
+ }
103
+
104
+ export { AzureTableTagCache as default };
@@ -0,0 +1,61 @@
1
+ import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
+ import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
+ interface AzureDeploymentConfig {
6
+ target?: AzureDeploymentTarget;
7
+ region?: string;
8
+ resourceGroup?: string;
9
+ }
10
+ interface AzureStorageConfig {
11
+ connectionString?: string;
12
+ accountName?: string;
13
+ accountKey?: string;
14
+ containerName?: string;
15
+ tableName?: string;
16
+ queueName?: string;
17
+ }
18
+ interface AzureConfig {
19
+ incrementalCache?: "azure-blob" | IncrementalCache;
20
+ tagCache?: "azure-table" | TagCache;
21
+ queue?: "azure-queue" | Queue;
22
+ routePreloadingBehavior?: RoutePreloadingBehavior;
23
+ middleware?: OpenNextConfig["middleware"];
24
+ dangerous?: OpenNextConfig["dangerous"];
25
+ buildCommand?: string;
26
+ buildOutputPath?: string;
27
+ appPath?: string;
28
+ packageJsonPath?: string;
29
+ deployment?: AzureDeploymentConfig;
30
+ storage?: AzureStorageConfig;
31
+ applicationInsights?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Defines the OpenNext configuration for Azure deployment.
36
+ *
37
+ * This extends the base OpenNext config with Azure-specific settings,
38
+ * using Azure Blob Storage, Table Storage, and Queue Storage by default.
39
+ */
40
+ declare function defineAzureConfig(config?: AzureConfig): OpenNextConfig;
41
+ /**
42
+ * Gets Azure configuration from environment variables.
43
+ * Used at runtime by the storage adapters.
44
+ */
45
+ declare function getAzureConfig(): {
46
+ deployment: {
47
+ target: any;
48
+ region: string;
49
+ };
50
+ storage: {
51
+ connectionString: string | undefined;
52
+ accountName: string | undefined;
53
+ accountKey: string | undefined;
54
+ containerName: string;
55
+ tableName: string;
56
+ queueName: string;
57
+ };
58
+ };
59
+
60
+ export { defineAzureConfig as d, getAzureConfig as g };
61
+ export type { AzureConfig as A, AzureDeploymentTarget as a };
@@ -0,0 +1,61 @@
1
+ import { RoutePreloadingBehavior, OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
2
+ import { IncrementalCache, TagCache, Queue } from '@opennextjs/aws/types/overrides.js';
3
+
4
+ type AzureDeploymentTarget = "functions" | "static-web-apps" | "container-apps";
5
+ interface AzureDeploymentConfig {
6
+ target?: AzureDeploymentTarget;
7
+ region?: string;
8
+ resourceGroup?: string;
9
+ }
10
+ interface AzureStorageConfig {
11
+ connectionString?: string;
12
+ accountName?: string;
13
+ accountKey?: string;
14
+ containerName?: string;
15
+ tableName?: string;
16
+ queueName?: string;
17
+ }
18
+ interface AzureConfig {
19
+ incrementalCache?: "azure-blob" | IncrementalCache;
20
+ tagCache?: "azure-table" | TagCache;
21
+ queue?: "azure-queue" | Queue;
22
+ routePreloadingBehavior?: RoutePreloadingBehavior;
23
+ middleware?: OpenNextConfig["middleware"];
24
+ dangerous?: OpenNextConfig["dangerous"];
25
+ buildCommand?: string;
26
+ buildOutputPath?: string;
27
+ appPath?: string;
28
+ packageJsonPath?: string;
29
+ deployment?: AzureDeploymentConfig;
30
+ storage?: AzureStorageConfig;
31
+ applicationInsights?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Defines the OpenNext configuration for Azure deployment.
36
+ *
37
+ * This extends the base OpenNext config with Azure-specific settings,
38
+ * using Azure Blob Storage, Table Storage, and Queue Storage by default.
39
+ */
40
+ declare function defineAzureConfig(config?: AzureConfig): OpenNextConfig;
41
+ /**
42
+ * Gets Azure configuration from environment variables.
43
+ * Used at runtime by the storage adapters.
44
+ */
45
+ declare function getAzureConfig(): {
46
+ deployment: {
47
+ target: any;
48
+ region: string;
49
+ };
50
+ storage: {
51
+ connectionString: string | undefined;
52
+ accountName: string | undefined;
53
+ accountKey: string | undefined;
54
+ containerName: string;
55
+ tableName: string;
56
+ queueName: string;
57
+ };
58
+ };
59
+
60
+ export { defineAzureConfig as d, getAzureConfig as g };
61
+ export type { AzureConfig as A, AzureDeploymentTarget as a };
@@ -0,0 +1,241 @@
1
+ // OpenNext Azure - Main Infrastructure Template
2
+ // This creates all resources needed to run a Next.js app on Azure
3
+ //
4
+ // DO NOT EDIT THIS FILE, IT IS AUTO-GENERATED.
5
+
6
+ @description('Name of the application (used as prefix for all resources)')
7
+ param appName string
8
+
9
+ @description('Location for all resources')
10
+ param location string = resourceGroup().location
11
+
12
+ @description('Environment (dev, staging, prod)')
13
+ @allowed(['dev', 'staging', 'prod'])
14
+ param environment string = 'dev'
15
+
16
+ @description('Node.js version for Functions')
17
+ @allowed(['20', '22'])
18
+ param nodeVersion string = '20'
19
+
20
+ @description('Enable Application Insights for monitoring and logging')
21
+ param enableApplicationInsights bool = false
22
+
23
+ // Variables
24
+ var uniqueSuffix = uniqueString(resourceGroup().id)
25
+ var sanitizedAppName = replace(toLower(appName), '-', '')
26
+ var maxAppNameLength = 24 - length(uniqueSuffix)
27
+ var truncatedAppName = length(sanitizedAppName) > maxAppNameLength
28
+ ? substring(sanitizedAppName, 0, maxAppNameLength)
29
+ : sanitizedAppName
30
+ var storageAccountName = '${truncatedAppName}${uniqueSuffix}'
31
+ var functionAppName = '${appName}-func-${environment}'
32
+ var appServicePlanName = '${appName}-plan-${environment}'
33
+ var containerName = 'nextjs-cache'
34
+ var tableName = 'nextjstags'
35
+ var queueName = 'nextjsrevalidation'
36
+ var applicationInsightsName = '${appName}-insights-${environment}'
37
+
38
+ // Application Insights (optional)
39
+ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = if (enableApplicationInsights) {
40
+ name: applicationInsightsName
41
+ location: location
42
+ kind: 'web'
43
+ properties: {
44
+ Application_Type: 'web'
45
+ Request_Source: 'rest'
46
+ RetentionInDays: environment == 'prod' ? 90 : 30
47
+ publicNetworkAccessForIngestion: 'Enabled'
48
+ publicNetworkAccessForQuery: 'Enabled'
49
+ }
50
+ }
51
+
52
+ // Storage Account (for cache, static assets, and function storage)
53
+ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
54
+ name: storageAccountName
55
+ location: location
56
+ sku: {
57
+ name: environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
58
+ }
59
+ kind: 'StorageV2'
60
+ properties: {
61
+ minimumTlsVersion: 'TLS1_2'
62
+ supportsHttpsTrafficOnly: true
63
+ allowBlobPublicAccess: true
64
+ accessTier: 'Hot'
65
+ }
66
+
67
+ // Blob service for cache and static assets
68
+ resource blobService 'blobServices' = {
69
+ name: 'default'
70
+ properties: {
71
+ cors: {
72
+ corsRules: [
73
+ {
74
+ allowedOrigins: ['*']
75
+ allowedMethods: ['GET', 'HEAD']
76
+ maxAgeInSeconds: 3600
77
+ exposedHeaders: ['*']
78
+ allowedHeaders: ['*']
79
+ }
80
+ ]
81
+ }
82
+ }
83
+
84
+ // Container for Next.js cache
85
+ resource cacheContainer 'containers' = {
86
+ name: containerName
87
+ properties: {
88
+ publicAccess: 'None'
89
+ }
90
+ }
91
+
92
+ // Container for static assets (public CDN access)
93
+ resource assetsContainer 'containers' = {
94
+ name: 'assets'
95
+ properties: {
96
+ publicAccess: 'Blob'
97
+ }
98
+ }
99
+ }
100
+
101
+ // Table service for tag cache
102
+ resource tableService 'tableServices' = {
103
+ name: 'default'
104
+
105
+ resource tagTable 'tables' = {
106
+ name: tableName
107
+ }
108
+ }
109
+
110
+ // Queue service for ISR revalidation (revalidateTag/revalidatePath)
111
+ resource queueService 'queueServices' = {
112
+ name: 'default'
113
+
114
+ resource revalidationQueue 'queues' = {
115
+ name: queueName
116
+ }
117
+ }
118
+ }
119
+
120
+ // App Service Plan (Consumption or Premium based on environment)
121
+ resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
122
+ name: appServicePlanName
123
+ location: location
124
+ sku: {
125
+ name: environment == 'prod' ? 'EP1' : 'Y1' // EP1 = Premium, Y1 = Consumption
126
+ tier: environment == 'prod' ? 'ElasticPremium' : 'Dynamic'
127
+ }
128
+ kind: 'functionapp'
129
+ properties: {
130
+ reserved: true // Linux
131
+ }
132
+ }
133
+
134
+ // Function App
135
+ resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
136
+ name: functionAppName
137
+ location: location
138
+ kind: 'functionapp,linux'
139
+ properties: {
140
+ serverFarmId: appServicePlan.id
141
+ siteConfig: {
142
+ linuxFxVersion: 'NODE|${nodeVersion}'
143
+ appSettings: concat([
144
+ {
145
+ name: 'AzureWebJobsStorage'
146
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
147
+ }
148
+ {
149
+ name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
150
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
151
+ }
152
+ {
153
+ name: 'WEBSITE_CONTENTSHARE'
154
+ value: toLower(functionAppName)
155
+ }
156
+ {
157
+ name: 'FUNCTIONS_EXTENSION_VERSION'
158
+ value: '~4'
159
+ }
160
+ {
161
+ name: 'FUNCTIONS_WORKER_RUNTIME'
162
+ value: 'node'
163
+ }
164
+ {
165
+ name: 'WEBSITE_NODE_DEFAULT_VERSION'
166
+ value: '~${nodeVersion}'
167
+ }
168
+ {
169
+ name: 'WEBSITE_RUN_FROM_PACKAGE'
170
+ value: '1'
171
+ }
172
+ {
173
+ name: 'AzureWebJobsDisableHomepage'
174
+ value: 'true'
175
+ }
176
+ // Next.js / OpenNext environment variables
177
+ {
178
+ name: 'AZURE_STORAGE_CONNECTION_STRING'
179
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net'
180
+ }
181
+ {
182
+ name: 'AZURE_STORAGE_ACCOUNT_NAME'
183
+ value: storageAccountName
184
+ }
185
+ {
186
+ name: 'AZURE_STORAGE_CONTAINER_NAME'
187
+ value: containerName
188
+ }
189
+ {
190
+ name: 'AZURE_TABLE_NAME'
191
+ value: tableName
192
+ }
193
+ {
194
+ name: 'AZURE_QUEUE_NAME'
195
+ value: queueName
196
+ }
197
+ {
198
+ name: 'NODE_ENV'
199
+ value: 'production'
200
+ }
201
+ ], enableApplicationInsights ? [
202
+ {
203
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
204
+ value: applicationInsights.properties.ConnectionString
205
+ }
206
+ {
207
+ name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
208
+ value: '~3'
209
+ }
210
+ ] : [])
211
+
212
+ ftpsState: 'Disabled'
213
+ minTlsVersion: '1.2'
214
+ cors: {
215
+ allowedOrigins: ['*']
216
+ }
217
+ }
218
+ httpsOnly: true
219
+ }
220
+ }
221
+
222
+ // Outputs
223
+ output functionAppName string = functionApp.name
224
+ output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}'
225
+ output storageAccountName string = storageAccount.name
226
+ output assetsUrl string = 'https://${storageAccount.name}.blob.${az.environment().suffixes.storage}/assets'
227
+ output resourceGroupName string = resourceGroup().name
228
+ output applicationInsightsName string = enableApplicationInsights ? applicationInsights.name : ''
229
+ output applicationInsightsInstrumentationKey string = enableApplicationInsights ? applicationInsights.properties.InstrumentationKey : ''
230
+
231
+ output deploymentInfo object = {
232
+ functionApp: functionAppName
233
+ storageAccount: storageAccountName
234
+ containerName: containerName
235
+ tableName: tableName
236
+ queueName: queueName
237
+ assetsUrl: 'https://${storageAccount.name}.blob.${az.environment().suffixes.storage}/assets'
238
+ functionUrl: 'https://${functionApp.properties.defaultHostName}'
239
+ appUrl: 'https://${functionApp.properties.defaultHostName}'
240
+ applicationInsights: enableApplicationInsights ? applicationInsightsName : null
241
+ }
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "opennextjs-azure",
3
+ "version": "0.1.1",
4
+ "description": "Azure adapter for Next.js applications using OpenNext",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "opennextjs-azure": "dist/cli/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json",
18
+ "./config/index.js": {
19
+ "import": "./dist/config/index.js",
20
+ "types": "./dist/config/index.d.ts"
21
+ },
22
+ "./adapters/wrappers/azure-functions.js": {
23
+ "import": "./dist/adapters/wrappers/azure-functions.js",
24
+ "types": "./dist/adapters/wrappers/azure-functions.d.ts"
25
+ },
26
+ "./adapters/converters/azure-http.js": {
27
+ "import": "./dist/adapters/converters/azure-http.js",
28
+ "types": "./dist/adapters/converters/azure-http.d.ts"
29
+ },
30
+ "./overrides/incrementalCache/azure-blob.js": {
31
+ "import": "./dist/overrides/incrementalCache/azure-blob.js",
32
+ "types": "./dist/overrides/incrementalCache/azure-blob.d.ts"
33
+ },
34
+ "./overrides/tagCache/azure-table.js": {
35
+ "import": "./dist/overrides/tagCache/azure-table.js",
36
+ "types": "./dist/overrides/tagCache/azure-table.d.ts"
37
+ },
38
+ "./overrides/queue/azure-queue.js": {
39
+ "import": "./dist/overrides/queue/azure-queue.js",
40
+ "types": "./dist/overrides/queue/azure-queue.d.ts"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "infrastructure"
46
+ ],
47
+ "scripts": {
48
+ "build": "unbuild",
49
+ "dev": "unbuild --stub",
50
+ "clean": "rimraf dist",
51
+ "lint": "eslint src --ext .ts",
52
+ "test": "vitest",
53
+ "typecheck": "tsc --noEmit",
54
+ "format": "prettier --write ."
55
+ },
56
+ "keywords": [
57
+ "nextjs",
58
+ "azure",
59
+ "serverless",
60
+ "azure-functions",
61
+ "opennext"
62
+ ],
63
+ "author": "",
64
+ "license": "MIT",
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "git+https://github.com/zpg6/opennextjs-azure.git"
68
+ },
69
+ "bugs": {
70
+ "url": "https://github.com/zpg6/opennextjs-azure/issues"
71
+ },
72
+ "homepage": "https://opennext.js.org/azure",
73
+ "dependencies": {
74
+ "@opennextjs/aws": "^3.8.5",
75
+ "@azure/functions": "^4.5.1",
76
+ "@azure/storage-blob": "^12.20.0",
77
+ "@azure/data-tables": "^13.2.2",
78
+ "@azure/storage-queue": "^12.18.0",
79
+ "commander": "^11.1.0"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^20.11.0",
83
+ "@typescript-eslint/eslint-plugin": "^6.20.0",
84
+ "@typescript-eslint/parser": "^6.20.0",
85
+ "eslint": "^8.56.0",
86
+ "rimraf": "^5.0.5",
87
+ "prettier": "^3.5.3",
88
+ "prettier-plugin-tailwindcss": "^0.5.0",
89
+ "typescript": "^5.3.3",
90
+ "unbuild": "^2.0.0",
91
+ "vitest": "^1.2.0"
92
+ },
93
+ "peerDependencies": {
94
+ "next": ">=13.4.0"
95
+ },
96
+ "engines": {
97
+ "node": ">=18.0.0"
98
+ }
99
+ }