├── .gitignore ├── src ├── build.js ├── index.ts ├── post-api.js ├── index.js ├── api-handler.ts └── api-handler.js ├── tsconfig.json ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | */.idea 4 | .idea/ 5 | 6 | # next.js 7 | /.next/ 8 | /out/ 9 | /api/ 10 | 11 | # misc 12 | .env -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { exec } = require("child_process"); 3 | (async () => { 4 | const execute = (command) => { 5 | return new Promise((resolve) => { 6 | exec(command, (error, stdout, stderr) => { 7 | if (error) { 8 | console.error(`exec error: ${error}`); 9 | return; 10 | } 11 | console.log(`stdout: ${stdout}`); 12 | console.log(`stderr: ${stderr}`); 13 | resolve(true); 14 | }); 15 | }); 16 | }; 17 | console.log("Build in progress..."); 18 | await execute("node ./node_modules/nextjs-azure-function/src/index.js && node ./node_modules/nextjs-azure-function/src/post-api.js") 19 | })() 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | import { Builder } from "@sls-next/lambda-at-edge"; 4 | import { join } from "path"; 5 | import { copy } from "fs-extra"; 6 | import { API_LAMBDA_CODE_DIR } from "@sls-next/lambda-at-edge/dist/build"; 7 | 8 | const main = async () => { 9 | const nextConfigPath = process.cwd(); 10 | 11 | const builder = new Builder( 12 | process.cwd(), 13 | join(nextConfigPath, ".serverless_nextjs"), 14 | { 15 | cmd: "node_modules/.bin/next", 16 | args: ["build"], 17 | useServerlessTraceTarget: true, 18 | } 19 | ); 20 | 21 | await builder.build(true); 22 | 23 | await copy( 24 | require.resolve("./api-handler.js"), 25 | join(builder.outputDir, API_LAMBDA_CODE_DIR, "index.js") 26 | ); 27 | } 28 | 29 | main(); 30 | 31 | export default "module"; 32 | -------------------------------------------------------------------------------- /src/post-api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | /* move directory into ./api */ 5 | 6 | 7 | const join = (str) => path.join(process.cwd(), str); 8 | fs.renameSync(join("./.serverless_nextjs/api-lambda"), join("./api")); 9 | 10 | /* create azure function configuration file. */ 11 | fs.mkdirSync(join("./api/function")); 12 | fs.writeFileSync( 13 | join("./api/function/function.json"), 14 | JSON.stringify( 15 | { 16 | bindings: [ 17 | { 18 | authLevel: "anonymous", 19 | type: "httpTrigger", 20 | direction: "in", 21 | name: "req", 22 | methods: ["get", "post", "put", "options", "delete", "patch"], 23 | route: "{*all}", 24 | }, 25 | { 26 | type: "http", 27 | direction: "out", 28 | name: "res", 29 | }, 30 | ], 31 | }, 32 | null, 33 | 4, 34 | ), 35 | "utf8", 36 | ); 37 | fs.renameSync(join("./api/index.js"), join("./api/function/function.js")); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": [ 5 | "esnext", 6 | "dom" 7 | ], 8 | "experimentalDecorators": true, 9 | "jsx": "preserve", 10 | "allowJs": true, 11 | "esModuleInterop": true, 12 | "target": "es6", 13 | "noImplicitAny": true, 14 | "moduleResolution": "node", 15 | "sourceMap": true, 16 | "strict": true, 17 | "outDir": "dist/src", 18 | "skipLibCheck": true, 19 | "allowSyntheticDefaultImports": true, 20 | "downlevelIteration": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "baseUrl": ".", 26 | "paths": { 27 | "@/*": [ 28 | "src/*" 29 | ], 30 | "api/*": [ 31 | "pages/api/*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "./**/*" 37 | ], 38 | "exclude": [ 39 | "./node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NextJS Azure Function 2 | 3 | Ports your nextjs api folder into a single azure function. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | npm i nextjs-azure-function 9 | npx nextjs-azure-function 10 | ``` 11 | 12 | 13 | ## Assumptions 14 | - Your pages/api folder isn't in src (support coming) 15 | 16 | ## FAQ 17 | 18 | **Q: Why does this compile into a single function instead of many?** 19 | 20 | **A:** Single function deploys on both AWS and Azure are considered by many to be the best practice, this 21 | particular function implements a "lazy-parse" meaning the only metric that will increase as your application grows 22 | is download time which is normally over azures gigabit network. This solution optimizes for cold starts. 23 | 24 | **Q: Does this handle getStaticProps/getInitialProps/...?** 25 | 26 | **A:** Not currently, however its pretty easy to support in the future. 27 | 28 | 29 | ## Shoutout 30 | - Huge thanks to the people maintaining the libraries this code relies on. 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-azure-function", 3 | "version": "1.0.12", 4 | "description": "Builds your NextJS /api pages/api folder into a single azure function", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc ./src/api-handler.ts --skipLibCheck true && tsc ./src/index.ts --skipLibCheck true" 8 | }, 9 | "bin": { 10 | "nextjs-azure-function": "src/build.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ShanonJackson/nextjs-azure-function.git" 15 | }, 16 | "keywords": [ 17 | "NextJS", 18 | "Azure", 19 | "Azure", 20 | "Function" 21 | ], 22 | "author": "Shanon Jackson", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/ShanonJackson/nextjs-azure-function/issues" 26 | }, 27 | "homepage": "https://github.com/ShanonJackson/nextjs-azure-function#readme", 28 | "devDependencies": { 29 | "typescript": "^4.1.3" 30 | }, 31 | "dependencies": { 32 | "@azure/functions": "^1.2.3", 33 | "@sls-next/lambda-at-edge": "^1.6.0-alpha.9", 34 | "fs-extra": "^9.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 40 | // @ts-nocheck 41 | var lambda_at_edge_1 = require("@sls-next/lambda-at-edge"); 42 | var path_1 = require("path"); 43 | var fs_extra_1 = require("fs-extra"); 44 | var build_1 = require("@sls-next/lambda-at-edge/dist/build"); 45 | var main = function () { return __awaiter(void 0, void 0, void 0, function () { 46 | var nextConfigPath, builder; 47 | return __generator(this, function (_a) { 48 | switch (_a.label) { 49 | case 0: 50 | nextConfigPath = process.cwd(); 51 | builder = new lambda_at_edge_1.Builder(process.cwd(), path_1.join(nextConfigPath, ".serverless_nextjs"), { 52 | cmd: "node_modules/.bin/next", 53 | args: ["build"], 54 | useServerlessTraceTarget: true 55 | }); 56 | return [4 /*yield*/, builder.build(true)]; 57 | case 1: 58 | _a.sent(); 59 | return [4 /*yield*/, fs_extra_1.copy(require.resolve("./api-handler.js"), path_1.join(builder.outputDir, build_1.API_LAMBDA_CODE_DIR, "index.js"))]; 60 | case 2: 61 | _a.sent(); 62 | return [2 /*return*/]; 63 | } 64 | }); 65 | }); }; 66 | main(); 67 | exports["default"] = "module"; 68 | -------------------------------------------------------------------------------- /src/api-handler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-ignore 4 | import { HttpRequest, Context } from "@azure/functions"; 5 | const manifest = require("../manifest.json"); 6 | const basePath = require("../routes-manifest.json"); 7 | const Stream = require("stream"); 8 | const http = require("http"); 9 | 10 | const AzureCompat = (azContext: Context, azReq: HttpRequest) => { 11 | azContext.res = azContext.res || {}; 12 | azContext.res.headers = azContext.res.headers || {}; 13 | const newStream = new Stream.Readable(); 14 | const req = Object.assign(newStream, http.IncomingMessage.prototype, { 15 | url: azReq.url, 16 | pathname: "/" + azReq.url.split("/").slice(3).join("/"), 17 | rawHeaders: [], 18 | headers: azReq.headers, 19 | method: azReq.method, 20 | log: azContext.log, 21 | getHeader: (name: string) => req.headers[name.toLowerCase()], 22 | getHeaders: () => azReq.headers, 23 | connection: {}, 24 | }); 25 | 26 | const res = new Stream(); 27 | Object.defineProperty(res, "statusCode", { 28 | get() { 29 | if (!azContext.res) return; 30 | return azContext.res.status; 31 | }, 32 | set(code) { 33 | if (!azContext.res) return; 34 | return (azContext.res.status = code); 35 | }, 36 | }); 37 | res.writeHead = (status: string, headers: any) => { 38 | if (!azContext.res) return; 39 | azContext.res.status = status; 40 | if (headers) azContext.res.headers = Object.assign(azContext.res.headers, headers); 41 | }; 42 | res.headers = {}; 43 | res.write = (chunk: any) => { 44 | if (!azContext.res) return; 45 | azContext.res.body = chunk; 46 | }; 47 | res.setHeader = (name: string, value: string) => { 48 | if (!azContext.res) return; 49 | return (azContext.res.headers[name.toLowerCase()] = value); 50 | }; 51 | res.removeHeader = (name: string) => { 52 | if (!azContext.res) return; 53 | delete azContext.res.headers[name.toLowerCase()]; 54 | }; 55 | res.getHeader = (name: string) => { 56 | if (!azContext.res) return; 57 | return azContext.res.headers[name.toLowerCase()]; 58 | }; 59 | res.getHeaders = () => { 60 | if (!azContext.res) return; 61 | return azContext.res.headers; 62 | }; 63 | res.hasHeader = (name: string) => !!res.getHeader(name); 64 | if (azReq.body) req.push(Buffer.from(JSON.stringify(azReq.body)), undefined); 65 | req.push(null); 66 | const promise = new Promise(function (resolve) { 67 | res.end = (text: any) => { 68 | if (azContext.res) azContext.res.body = text; 69 | resolve(azContext); 70 | }; 71 | }); 72 | return { req: req, res: res, promise: promise }; 73 | }; 74 | 75 | const handlerFactory = (page: any) => async (azContext: Context, azReq: HttpRequest) => { 76 | const { req, res, promise } = AzureCompat(azContext, azReq); 77 | if (page.render instanceof Function) { 78 | page.render(req, res); 79 | } else { 80 | page.default(req, res); 81 | } 82 | return promise; 83 | }; 84 | // end api-gw compat code 85 | 86 | const normaliseUri = (uri: string): string => (uri === "/" ? "/index" : uri); 87 | 88 | const router = (manifest: any): ((path: string) => string | null) => { 89 | const { 90 | apis: { dynamic, nonDynamic }, 91 | } = manifest; 92 | return (path: string): string | null => { 93 | if (basePath && path.startsWith(basePath)) path = path.slice(basePath.length); 94 | if (nonDynamic[path]) { 95 | return nonDynamic[path]; 96 | } 97 | for (const route in dynamic) { 98 | const { file, regex } = dynamic[route]; 99 | const re = new RegExp(regex, "i"); 100 | const pathMatchesRoute = re.test(path); 101 | 102 | if (pathMatchesRoute) { 103 | return file; 104 | } 105 | } 106 | return null; 107 | }; 108 | }; 109 | 110 | module.exports = async (context: Context, req: HttpRequest) => { 111 | const uri = normaliseUri("/" + req.url.split("?")[0].split("/").slice(3).join("/")); 112 | const pagePath = router(manifest)(uri); 113 | if (!pagePath) { 114 | return { res: { status: 404 } }; 115 | } 116 | // eslint-disable-next-line 117 | const page = require(`../${pagePath}`); 118 | return handlerFactory(page)(context, req); 119 | }; 120 | -------------------------------------------------------------------------------- /src/api-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | var manifest = require("../manifest.json"); 40 | var basePath = require("../routes-manifest.json"); 41 | var Stream = require("stream"); 42 | var http = require("http"); 43 | var AzureCompat = function (azContext, azReq) { 44 | azContext.res = azContext.res || {}; 45 | azContext.res.headers = azContext.res.headers || {}; 46 | var newStream = new Stream.Readable(); 47 | var req = Object.assign(newStream, http.IncomingMessage.prototype, { 48 | url: azReq.url, 49 | pathname: "/" + azReq.url.split("/").slice(3).join("/"), 50 | rawHeaders: [], 51 | headers: azReq.headers, 52 | method: azReq.method, 53 | log: azContext.log, 54 | getHeader: function (name) { return req.headers[name.toLowerCase()]; }, 55 | getHeaders: function () { return azReq.headers; }, 56 | connection: {} 57 | }); 58 | var res = new Stream(); 59 | Object.defineProperty(res, "statusCode", { 60 | get: function () { 61 | if (!azContext.res) 62 | return; 63 | return azContext.res.status; 64 | }, 65 | set: function (code) { 66 | if (!azContext.res) 67 | return; 68 | return (azContext.res.status = code); 69 | } 70 | }); 71 | res.writeHead = function (status, headers) { 72 | if (!azContext.res) 73 | return; 74 | azContext.res.status = status; 75 | if (headers) 76 | azContext.res.headers = Object.assign(azContext.res.headers, headers); 77 | }; 78 | res.headers = {}; 79 | res.write = function (chunk) { 80 | if (!azContext.res) 81 | return; 82 | azContext.res.body = chunk; 83 | }; 84 | res.setHeader = function (name, value) { 85 | if (!azContext.res) 86 | return; 87 | return (azContext.res.headers[name.toLowerCase()] = value); 88 | }; 89 | res.removeHeader = function (name) { 90 | if (!azContext.res) 91 | return; 92 | delete azContext.res.headers[name.toLowerCase()]; 93 | }; 94 | res.getHeader = function (name) { 95 | if (!azContext.res) 96 | return; 97 | return azContext.res.headers[name.toLowerCase()]; 98 | }; 99 | res.getHeaders = function () { 100 | if (!azContext.res) 101 | return; 102 | return azContext.res.headers; 103 | }; 104 | res.hasHeader = function (name) { return !!res.getHeader(name); }; 105 | if (azReq.body) 106 | req.push(Buffer.from(JSON.stringify(azReq.body)), undefined); 107 | req.push(null); 108 | var promise = new Promise(function (resolve) { 109 | res.end = function (text) { 110 | if (azContext.res) 111 | azContext.res.body = text; 112 | resolve(azContext); 113 | }; 114 | }); 115 | return { req: req, res: res, promise: promise }; 116 | }; 117 | var handlerFactory = function (page) { return function (azContext, azReq) { return __awaiter(void 0, void 0, void 0, function () { 118 | var _a, req, res, promise; 119 | return __generator(this, function (_b) { 120 | _a = AzureCompat(azContext, azReq), req = _a.req, res = _a.res, promise = _a.promise; 121 | if (page.render instanceof Function) { 122 | page.render(req, res); 123 | } 124 | else { 125 | page["default"](req, res); 126 | } 127 | return [2 /*return*/, promise]; 128 | }); 129 | }); }; }; 130 | // end api-gw compat code 131 | var normaliseUri = function (uri) { return (uri === "/" ? "/index" : uri); }; 132 | var router = function (manifest) { 133 | var _a = manifest.apis, dynamic = _a.dynamic, nonDynamic = _a.nonDynamic; 134 | return function (path) { 135 | if (basePath && path.startsWith(basePath)) 136 | path = path.slice(basePath.length); 137 | if (nonDynamic[path]) { 138 | return nonDynamic[path]; 139 | } 140 | for (var route in dynamic) { 141 | var _a = dynamic[route], file = _a.file, regex = _a.regex; 142 | var re = new RegExp(regex, "i"); 143 | var pathMatchesRoute = re.test(path); 144 | if (pathMatchesRoute) { 145 | return file; 146 | } 147 | } 148 | return null; 149 | }; 150 | }; 151 | module.exports = function (context, req) { return __awaiter(void 0, void 0, void 0, function () { 152 | var uri, pagePath, page; 153 | return __generator(this, function (_a) { 154 | uri = normaliseUri("/" + req.url.split("?")[0].split("/").slice(3).join("/")); 155 | pagePath = router(manifest)(uri); 156 | if (!pagePath) { 157 | return [2 /*return*/, { res: { status: 404 } }]; 158 | } 159 | page = require("../" + pagePath); 160 | return [2 /*return*/, handlerFactory(page)(context, req)]; 161 | }); 162 | }); }; 163 | --------------------------------------------------------------------------------