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

View File

@@ -49,6 +49,8 @@ export interface Config {
debug?: boolean; debug?: boolean;
zeroConfig?: boolean; zeroConfig?: boolean;
import?: { [key: string]: string }; import?: { [key: string]: string };
memory?: number;
maxDuration?: number;
} }
export interface Meta { export interface Meta {
@@ -308,3 +310,11 @@ export interface Builder {
src: string; src: string;
config?: Config; 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].use).toBe('@now/static');
expect(builders[1].src).toBe('!{api/**,package.json}'); 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 () => { it('Test `detectRoutes`', async () => {