Skip to content

Commit

Permalink
configurable serverless runtime nodejs version
Browse files Browse the repository at this point in the history
  • Loading branch information
thescientist13 committed Nov 28, 2024
1 parent 1e9a20b commit 67c4635
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 5 deletions.
19 changes: 19 additions & 0 deletions packages/plugin-adapter-vercel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,26 @@ export default {
}
```

## Options

### Runtime

Vercel supports [multiple semver major NodeJS versions](https://vercel.com/docs/functions/runtimes/node-js/node-js-versions#default-and-available-versions) for the serverless runtime as part of the [build output API](https://vercel.com/docs/build-output-api/v3/primitives#serverless-functions). With the **runtime** option, you can configure your functions for any supported NodeJS version. Current default version is `nodejs20.x`.

```javascript
import { greenwoodPluginAdapterVercel } from '@greenwood/plugin-adapter-vercel';

export default {
plugins: [
greenwoodPluginAdapterVercel({
runtime: 'nodejs22.x'
})
]
}
```

## Caveats

1. [Edge runtime](https://vercel.com/docs/concepts/functions/edge-functions) is not supported ([yet](https://github.com/ProjectEvergreen/greenwood/issues/1141)).
1. The Vercel CLI (`vercel dev`) is not compatible with Build Output v3.
```sh
Expand Down
11 changes: 7 additions & 4 deletions packages/plugin-adapter-vercel/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import fs from 'fs/promises';
import path from 'path';
import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js';

const DEFAULT_RUNTIME = 'nodejs20.x';

// https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-helpers
function generateOutputFormat(id, type) {
const handlerAlias = '$handler';
Expand Down Expand Up @@ -51,7 +53,7 @@ function generateOutputFormat(id, type) {
`;
}

async function setupFunctionBuildFolder(id, outputType, outputRoot) {
async function setupFunctionBuildFolder(id, outputType, outputRoot, runtime) {
const outputFormat = generateOutputFormat(id, outputType);

await fs.mkdir(outputRoot, { recursive: true });
Expand All @@ -60,14 +62,15 @@ async function setupFunctionBuildFolder(id, outputType, outputRoot) {
type: 'module'
}));
await fs.writeFile(new URL('./.vc-config.json', outputRoot), JSON.stringify({
runtime: 'nodejs18.x',
runtime,
handler: 'index.js',
launcherType: 'Nodejs',
shouldAddHelpers: true
}));
}

async function vercelAdapter(compilation) {
async function vercelAdapter(compilation, options) {
const { runtime = DEFAULT_RUNTIME } = options;
const { outputDir, projectDirectory } = compilation.context;
const { basePath } = compilation.config;
const adapterOutputUrl = new URL('./.vercel/output/functions/', projectDirectory);
Expand All @@ -89,7 +92,7 @@ async function vercelAdapter(compilation) {
const chunks = (await fs.readdir(outputDir))
.filter(file => file.startsWith(`${id}.route.chunk`) && file.endsWith('.js'));

await setupFunctionBuildFolder(id, outputType, outputRoot);
await setupFunctionBuildFolder(id, outputType, outputRoot, runtime);

// handle user's actual route entry file
await fs.cp(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Use Case
* Run Greenwood with the Vercel adapter plugin and setting the runtime option.
*
* User Result
* Should generate a static Greenwood build with serverless and edge functions output.
*
* User Command
* greenwood build
*
* User Config
* import { greenwoodPluginAdapterVercel } from '@greenwood/plugin-adapter-vercel';
*
* {
* plugins: [{
* greenwoodPluginAdapterVercel({
* runtime: 'nodejs22.x
* })
* }]
* }
*
* User Workspace
* package.json
* src/
* pages/
* index.js
*/
import chai from 'chai';
import fs from 'fs/promises';
import glob from 'glob-promise';
import { JSDOM } from 'jsdom';
import path from 'path';
import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js';
import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import { normalizePathnameForWindows } from '@greenwood/cli/src/lib/resource-utils.js';
import { Runner } from 'gallinago';
import { fileURLToPath } from 'url';

const expect = chai.expect;

describe('Build Greenwood With: ', function() {
const LABEL = 'Vercel Adapter plugin output with runtime option set';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
const vercelOutputFolder = new URL('./.vercel/output/', import.meta.url);
const vercelFunctionsOutputUrl = new URL('./functions/', vercelOutputFolder);
const hostname = 'http://www.example.com';
let runner;

before(function() {
this.context = {
publicDir: path.join(outputPath, 'public')
};
runner = new Runner();
});

describe(LABEL, function() {
before(function() {
runner.setup(outputPath, getSetupFiles(outputPath));
runner.runCommand(cliPath, 'build');
});

describe('Default Output', function() {
let configFile;
let functionFolders;

before(async function() {
configFile = await fs.readFile(new URL('./config.json', vercelOutputFolder), 'utf-8');
functionFolders = await glob.promise(path.join(normalizePathnameForWindows(vercelFunctionsOutputUrl), '**/*.func'));
});

it('should output the expected number of serverless function output folders', function() {
expect(functionFolders.length).to.be.equal(1);
});

it('should output the expected configuration file for the build output', function() {
expect(configFile).to.be.equal('{"version":3}');
});

it('should output the expected package.json for each serverless function', function() {
functionFolders.forEach(async (folder) => {
const packageJson = await fs.readFile(new URL('./package.json', `file://${folder}/`), 'utf-8');

expect(packageJson).to.be.equal('{"type":"module"}');
});
});

it('should output the expected .vc-config.json for each serverless function with runtime option honored', function() {
functionFolders.forEach(async (folder) => {
const packageJson = await fs.readFile(new URL('./vc-config.json', `file://${folder}/`), 'utf-8');

expect(packageJson).to.be.equal('{"runtime":"nodejs22.x","handler":"index.js","launcherType":"Nodejs","shouldAddHelpers":true}');
});
});
});

describe('Static directory output', function() {
it('should return the expected response when the serverless adapter entry point handler is invoked', async function() {
const publicFiles = await glob.promise(path.join(outputPath, 'public/**/**'));

for (const file of publicFiles) {
const buildOutputDestination = file.replace(path.join(outputPath, 'public'), path.join(vercelOutputFolder.pathname, 'static'));
const itExists = await checkResourceExists(new URL(`file://${buildOutputDestination}`));

expect(itExists).to.be.equal(true);
}
});
});

describe('Index SSR Page adapter', function() {
it('should return the expected response when the serverless adapter entry point handler is invoked', async function() {
const handler = (await import(new URL('./index.func/index.js', vercelFunctionsOutputUrl))).default;
const response = {
headers: new Headers()
};

await handler({
url: `${hostname}/`,
headers: {
host: hostname
},
method: 'GET'
}, {
status: function(code) {
response.status = code;
},
send: function(body) {
response.body = body;
},
setHeader: function(key, value) {
response.headers.set(key, value);
}
});

const { status, body, headers } = response;
const dom = new JSDOM(body);
const headings = dom.window.document.querySelectorAll('body > h1');

expect(status).to.be.equal(200);
expect(headers.get('content-type')).to.be.equal('text/html');

expect(headings.length).to.be.equal(1);
expect(headings[0].textContent).to.be.equal('Just here causing trouble! :D');
});
});
});

after(function() {
runner.teardown([
path.join(outputPath, '.vercel'),
...getOutputTeardownFiles(outputPath)
]);
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { greenwoodPluginAdapterVercel } from '../../../src/index.js';

export default {
plugins: [
greenwoodPluginAdapterVercel({
runtime: 'nodejs22.x'
})
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class IndexPage extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>Just here causing trouble! :D</h1>';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('Build Greenwood With: ', function() {
functionFolders.forEach(async (folder) => {
const packageJson = await fs.readFile(new URL('./vc-config.json', `file://${folder}/`), 'utf-8');

expect(packageJson).to.be.equal('{"runtime":"nodejs18.x","handler":"index.js","launcherType":"Nodejs","shouldAddHelpers":true}');
expect(packageJson).to.be.equal('{"runtime":"nodejs20.x","handler":"index.js","launcherType":"Nodejs","shouldAddHelpers":true}');
});
});
});
Expand Down

0 comments on commit 67c4635

Please sign in to comment.