mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 21:07:48 +00:00
[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:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
44
packages/now-build-utils/test/unit.test.js
vendored
44
packages/now-build-utils/test/unit.test.js
vendored
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user