[now-build-utils] Add functions property (#3213)

This updates `@now/build-utils` to add support for the function property including types and tests.

Related: [PRODUCT-27]

[PRODUCT-27]: https://zeit.atlassian.net/browse/PRODUCT-27
This commit is contained in:
Andy
2019-10-29 00:47:45 +01:00
committed by Leo Lamprecht
parent 818018ce42
commit f2bd36b4f9
3 changed files with 121 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
import { PackageJson, Builder, Config } from './types';
import minimatch from 'minimatch';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
interface ErrorResponse {
code: string;
@@ -8,6 +8,7 @@ interface ErrorResponse {
interface Options {
tag?: 'canary' | 'latest' | string;
functions?: BuilderFunctions;
}
const src = 'package.json';
@@ -21,21 +22,25 @@ const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
};
// Static builders are special cased in `@now/static-build`
function getBuilders(): Map<string, Builder> {
function getBuilders({ tag }: Options = {}): Map<string, Builder> {
const withTag = tag ? `@${tag}` : '';
return new Map<string, Builder>([
['next', { src, use: '@now/next', config }],
['next', { src, use: `@now/next${withTag}`, config }],
]);
}
// Must be a function to ensure that the returned
// object won't be a reference
function getApiBuilders(): Builder[] {
function getApiBuilders({ tag }: Options = {}): Builder[] {
const withTag = tag ? `@${tag}` : '';
return [
{ src: 'api/**/*.js', use: '@now/node', config },
{ src: 'api/**/*.ts', use: '@now/node', config },
{ src: 'api/**/*.go', use: '@now/go', config },
{ src: 'api/**/*.py', use: '@now/python', config },
{ src: 'api/**/*.rb', use: '@now/ruby', config },
{ src: 'api/**/*.js', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.ts', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.go', use: `@now/go${withTag}`, config },
{ src: 'api/**/*.py', use: `@now/python${withTag}`, config },
{ src: 'api/**/*.rb', use: `@now/ruby${withTag}`, config },
];
}
@@ -48,13 +53,50 @@ function hasBuildScript(pkg: PackageJson | undefined) {
return Boolean(scripts && scripts['build']);
}
async function detectBuilder(pkg: PackageJson): Promise<Builder> {
for (const [dependency, builder] of getBuilders()) {
function getFunctionBuilder(
file: string,
prevBuilder: Builder | undefined,
{ functions = {} }: Options
) {
const key = Object.keys(functions).find(
k => minimatch(file, k) || file === k
);
const fn = key ? functions[key] : undefined;
if (!fn || (!fn.runtime && !prevBuilder)) {
return prevBuilder;
}
const src = (prevBuilder && prevBuilder.src) || file;
const use = fn.runtime || (prevBuilder && prevBuilder.use);
const config: Config = Object.assign({}, prevBuilder && prevBuilder.config);
if (!use) {
return prevBuilder;
}
if (fn.memory) {
config.memory = fn.memory;
}
if (fn.maxDuration) {
config.maxDuration = fn.maxDuration;
}
return { use, src, config };
}
async function detectFrontBuilder(
pkg: PackageJson,
options: Options
): Promise<Builder> {
for (const [dependency, builder] of getBuilders(options)) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
const fnBuilder = getFunctionBuilder('package.json', builder, options);
// Return the builder when a dependency matches
if (deps[dependency]) {
return builder;
return fnBuilder || builder;
}
}
@@ -91,16 +133,19 @@ export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB);
}
async function detectApiBuilders(files: string[]): Promise<Builder[]> {
async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
const builds = files
.sort(sortFiles)
.filter(ignoreApiFilter)
.map(file => {
const result = getApiBuilders().find(({ src }): boolean =>
minimatch(file, src)
const apiBuilder = getApiBuilders(options).find(b =>
minimatch(file, b.src)
);
return result ? { ...result, src: file } : null;
const fnBuilder = getFunctionBuilder(file, apiBuilder, options);
return fnBuilder ? { ...fnBuilder, src: file } : null;
});
const finishedBuilds = builds.filter(Boolean);
@@ -114,11 +159,9 @@ async function checkConflictingFiles(
builders: Builder[]
): Promise<ErrorResponse | null> {
// For Next.js
if (builders.some(builder => builder.use.startsWith('@now/next'))) {
if (builders.some(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
const hasApiBuilders = builders.some(builder =>
builder.src.startsWith('api/')
);
const hasApiBuilders = builders.some(b => b.src.startsWith('api/'));
if (hasApiPages && hasApiBuilders) {
return {
@@ -137,7 +180,7 @@ async function checkConflictingFiles(
export async function detectBuilders(
files: string[],
pkg?: PackageJson | undefined | null,
options?: Options
options: Options = {}
): Promise<{
builders: Builder[] | null;
errors: ErrorResponse[] | null;
@@ -147,10 +190,10 @@ export async function detectBuilders(
const warnings: ErrorResponse[] = [];
// Detect all builders for the `api` directory before anything else
let builders = await detectApiBuilders(files);
const builders = await detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) {
builders.push(await detectBuilder(pkg));
builders.push(await detectFrontBuilder(pkg, options));
const conflictError = await checkConflictingFiles(files, builders);
@@ -187,25 +230,6 @@ export async function detectBuilders(
}
}
// Change the tag for the builders
if (builders && builders.length) {
const tag = options && options.tag;
if (tag) {
builders = builders.map((originBuilder: Builder) => {
// Copy builder to make sure it is not a reference
const builder = { ...originBuilder };
// @now/static has no canary builder
if (builder.use !== '@now/static') {
builder.use = `${builder.use}@${tag}`;
}
return builder;
});
}
}
return {
builders: builders.length ? builders : null,
errors: errors.length ? errors : null,

View File

@@ -49,6 +49,8 @@ export interface Config {
debug?: boolean;
zeroConfig?: boolean;
import?: { [key: string]: string };
memory?: number;
maxDuration?: number;
}
export interface Meta {
@@ -308,3 +310,11 @@ export interface Builder {
src: string;
config?: Config;
}
export interface BuilderFunctions {
[key: string]: {
memory?: number;
maxDuration?: number;
runtime?: string;
};
}

View File

@@ -444,6 +444,50 @@ it('Test `detectBuilders`', async () => {
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('!{api/**,package.json}');
}
{
// extend with functions
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const functions = {
'api/users/*.ts': {
runtime: 'my-custom-runtime-package',
},
'api/teams/members.ts': {
memory: 128,
maxDuration: 10,
},
'package.json': {
memory: 3008,
runtime: '@now/next@canary',
},
};
const files = [
'pages/index.js',
'api/users/[id].ts',
'api/teams/members.ts',
];
const { builders } = await detectBuilders(files, pkg, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
src: 'api/teams/members.ts',
use: '@now/node',
config: { zeroConfig: true, memory: 128, maxDuration: 10 },
});
expect(builders[1]).toEqual({
src: 'api/users/[id].ts',
use: 'my-custom-runtime-package',
config: { zeroConfig: true },
});
expect(builders[2]).toEqual({
src: 'package.json',
use: '@now/next@canary',
config: { zeroConfig: true, memory: 3008 },
});
}
});
it('Test `detectRoutes`', async () => {