Files
vercel/packages/cli/test/unit/commands/build/index.test.ts
Nathan Rajlich b9f3438c2d [cli] Fix framework version detection in monorepos (#11212)
I noticed that the framework version was not being printed on deployments in a monorepo. Turns out the framework detection logic was happening at the root of the monorepo, instead of the project directory.
2024-03-12 22:54:53 +00:00

1324 lines
41 KiB
TypeScript

import fs from 'fs-extra';
import { join } from 'path';
import { getWriteableDirectory } from '@vercel/build-utils';
import build from '../../../../src/commands/build';
import { client } from '../../../mocks/client';
import { defaultProject, useProject } from '../../../mocks/project';
import { useTeams } from '../../../mocks/team';
import { useUser } from '../../../mocks/user';
import { execSync } from 'child_process';
jest.setTimeout(6 * 60 * 1000);
const fixture = (name: string) =>
join(__dirname, '../../../fixtures/unit/commands/build', name);
describe('build', () => {
beforeEach(() => {
delete process.env.__VERCEL_BUILD_RUNNING;
});
it('should build with `@vercel/static`', async () => {
const cwd = fixture('static');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static',
apiVersion: 2,
src: '**',
use: '@vercel/static',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.html']);
});
it('should build with `@now/static`', async () => {
const cwd = fixture('now-static');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@now/static',
apiVersion: 2,
src: 'www/index.html',
use: '@now/static',
},
],
});
const files = await fs.readdir(join(output, 'static'));
expect(files).toEqual(['www']);
const www = await fs.readdir(join(output, 'static', 'www'));
expect(www).toEqual(['index.html']);
});
it('should build with `@vercel/node`', async () => {
const cwd = fixture('node');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/node" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'api/es6.js',
config: { zeroConfig: true },
},
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'api/index.js',
config: { zeroConfig: true },
},
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'api/mjs.mjs',
config: { zeroConfig: true },
},
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'api/typescript.ts',
config: { zeroConfig: true },
},
],
});
// "static" directory is empty
const hasStaticFiles = await fs.pathExists(join(output, 'static'));
expect(
hasStaticFiles,
'Expected ".vercel/output/static" to not exist'
).toEqual(false);
// "functions/api" directory has output Functions
const functions = await fs.readdir(join(output, 'functions/api'));
expect(functions.sort()).toEqual([
'es6.func',
'index.func',
'mjs.func',
'typescript.func',
]);
});
it('should handle symlinked static files', async () => {
const cwd = fixture('static-symlink');
const output = join(cwd, '.vercel/output');
// try to create the symlink, if it fails (e.g. Windows), skip the test
try {
await fs.unlink(join(cwd, 'foo.html'));
await fs.symlink(join(cwd, 'index.html'), join(cwd, 'foo.html'));
} catch (e) {
console.log('Symlinks not available, skipping test');
return;
}
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static',
apiVersion: 2,
src: '**',
use: '@vercel/static',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['foo.html', 'index.html']);
expect(
(await fs.lstat(join(output, 'static', 'foo.html'))).isSymbolicLink()
).toEqual(true);
expect(
(await fs.lstat(join(output, 'static', 'index.html'))).isSymbolicLink()
).toEqual(false);
});
it('should normalize "src" path in `vercel.json`', async () => {
const cwd = fixture('normalize-src');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/node" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'server.js',
},
],
});
// `config.json` includes "route" from `vercel.json`
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toMatchObject({
version: 3,
routes: [
{
src: '^/(.*)$',
dest: '/server.js',
},
],
});
// "static" directory is empty
const hasStaticFiles = await fs.pathExists(join(output, 'static'));
expect(
hasStaticFiles,
'Expected ".vercel/output/static" to not exist'
).toEqual(false);
// "functions" directory has output Function
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['server.js.func']);
});
it('should build with 3rd party Builder', async () => {
const cwd = fixture('third-party-builder');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "txt-builder" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: 'txt-builder',
apiVersion: 3,
use: 'txt-builder@0.0.0',
src: 'api/foo.txt',
config: {
zeroConfig: true,
functions: {
'api/*.txt': {
runtime: 'txt-builder@0.0.0',
},
},
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// "static" directory is empty
const hasStaticFiles = await fs.pathExists(join(output, 'static'));
expect(
hasStaticFiles,
'Expected ".vercel/output/static" to not exist'
).toEqual(false);
// "functions/api" directory has output Functions
const functions = await fs.readdir(join(output, 'functions/api'));
expect(functions.sort()).toEqual(['foo.func']);
const vcConfig = await fs.readJSON(
join(output, 'functions/api/foo.func/.vc-config.json')
);
expect(vcConfig).toMatchObject({
handler: 'api/foo.txt',
runtime: 'provided',
environment: {},
});
});
it('should serialize `EdgeFunction` output in version 3 Builder', async () => {
const cwd = fixture('edge-function');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
client.setArgv('build', '--prod');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "edge-function" Builder was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'production',
builds: [
{
require: 'edge-function',
apiVersion: 3,
use: 'edge-function@0.0.0',
src: 'api/edge.js',
config: {
zeroConfig: true,
functions: {
'api/*.js': {
runtime: 'edge-function@0.0.0',
},
},
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// "static" directory is empty
const hasStaticFiles = await fs.pathExists(join(output, 'static'));
expect(
hasStaticFiles,
'Expected ".vercel/output/static" to not exist'
).toEqual(false);
// "functions/api" directory has output Functions
const functions = await fs.readdir(join(output, 'functions/api'));
expect(functions.sort()).toEqual(['edge.func']);
const vcConfig = await fs.readJSON(
join(output, 'functions/api/edge.func/.vc-config.json')
);
expect(vcConfig).toMatchObject({
runtime: 'edge',
name: 'api/edge.js',
deploymentTarget: 'v8-worker',
entrypoint: 'api/edge.js',
});
});
it('should pull "preview" env vars by default', async () => {
const cwd = fixture('static-pull');
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'vercel-pull-next',
name: 'vercel-pull-next',
});
const envFilePath = join(cwd, '.vercel', '.env.preview.local');
const projectJsonPath = join(cwd, '.vercel', 'project.json');
const originalProjectJson = await fs.readJSON(
join(cwd, '.vercel/project.json')
);
try {
client.cwd = cwd;
client.setArgv('build', '--yes');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const previewEnv = await fs.readFile(envFilePath, 'utf8');
const envFileHasPreviewEnv = previewEnv.includes(
'REDIS_CONNECTION_STRING'
);
expect(envFileHasPreviewEnv).toBeTruthy();
} finally {
await fs.remove(envFilePath);
await fs.writeJSON(projectJsonPath, originalProjectJson, { spaces: 2 });
}
});
it('should pull "production" env vars with `--prod`', async () => {
const cwd = fixture('static-pull');
useUser();
useTeams('team_dummy');
useProject({
...defaultProject,
id: 'vercel-pull-next',
name: 'vercel-pull-next',
});
const envFilePath = join(cwd, '.vercel', '.env.production.local');
const projectJsonPath = join(cwd, '.vercel', 'project.json');
const originalProjectJson = await fs.readJSON(
join(cwd, '.vercel/project.json')
);
try {
client.cwd = cwd;
client.setArgv('build', '--yes', '--prod');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const prodEnv = await fs.readFile(envFilePath, 'utf8');
const envFileHasProductionEnv1 = prodEnv.includes(
'REDIS_CONNECTION_STRING'
);
expect(envFileHasProductionEnv1).toBeTruthy();
const envFileHasProductionEnv2 = prodEnv.includes(
'SQL_CONNECTION_STRING'
);
expect(envFileHasProductionEnv2).toBeTruthy();
} finally {
await fs.remove(envFilePath);
await fs.writeJSON(projectJsonPath, originalProjectJson, { spaces: 2 });
}
});
it('should build root-level `middleware.js` and exclude from static files', async () => {
const cwd = fixture('middleware');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/node" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'middleware.js',
config: {
zeroConfig: true,
middleware: true,
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// `config.json` includes the "middlewarePath" route
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toMatchObject({
version: 3,
routes: [
{
src: '^/.*$',
middlewarePath: 'middleware',
middlewareRawSrc: [],
override: true,
continue: true,
},
{ handle: 'error' },
{ status: 404, src: '^(?!/api).*$', dest: '/404.html' },
],
});
// "static" directory contains `index.html`, but *not* `middleware.js`
const staticFiles = await fs.readdir(join(output, 'static'));
expect(staticFiles.sort()).toEqual(['index.html']);
// "functions" directory contains `middleware.func`
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['middleware.func']);
});
it('should build root-level `middleware.js` with "Root Directory" setting', async () => {
const cwd = fixture('middleware-root-directory');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'middleware.js',
config: {
zeroConfig: true,
middleware: true,
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// `config.json` includes the "middlewarePath" route
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toMatchObject({
version: 3,
routes: [
{
src: '^/.*$',
middlewarePath: 'middleware',
middlewareRawSrc: [],
override: true,
continue: true,
},
{ handle: 'error' },
{ status: 404, src: '^(?!/api).*$', dest: '/404.html' },
],
});
// "static" directory contains `index.html`, but *not* `middleware.js`
const staticFiles = await fs.readdir(join(output, 'static'));
expect(staticFiles.sort()).toEqual(['index.html']);
// "functions" directory contains `middleware.func`
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['middleware.func']);
});
it('should build root-level `middleware.js` with "matcher" config', async () => {
const cwd = fixture('middleware-with-matcher');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/node" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/node',
apiVersion: 3,
use: '@vercel/node',
src: 'middleware.js',
config: {
zeroConfig: true,
middleware: true,
},
},
{
require: '@vercel/static',
apiVersion: 2,
use: '@vercel/static',
src: '!{api/**,package.json,middleware.[jt]s}',
config: {
zeroConfig: true,
},
},
],
});
// `config.json` includes the "middlewarePath" route
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toMatchObject({
version: 3,
routes: [
{
src: '^\\/about(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$|^\\/dashboard(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?[\\/#\\?]?$',
middlewarePath: 'middleware',
middlewareRawSrc: ['/about/:path*', '/dashboard/:path*'],
override: true,
continue: true,
},
{ handle: 'error' },
{ status: 404, src: '^(?!/api).*$', dest: '/404.html' },
],
});
// "static" directory contains `index.html`, but *not* `middleware.js`
const staticFiles = await fs.readdir(join(output, 'static'));
expect(staticFiles.sort()).toEqual(['index.html']);
// "functions" directory contains `middleware.func`
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['middleware.func']);
});
it('should support `--output` parameter', async () => {
const cwd = fixture('static');
const output = await getWriteableDirectory();
try {
client.cwd = cwd;
client.setArgv('build', '--output', output);
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static',
apiVersion: 2,
src: '**',
use: '@vercel/static',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.html']);
} finally {
await fs.remove(output);
}
});
// This test is for `vercel-sapper` which doesn't export `version` property,
// but returns a structure that's compatible with `version: 2`
it("should support Builder that doesn't export `version`", async () => {
const cwd = fixture('versionless-builder');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "versionless-builder" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: 'versionless-builder',
src: 'package.json',
use: 'versionless-builder@0.0.0',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['file']);
expect(await fs.readFile(join(output, 'static/file'), 'utf8')).toEqual(
'file contents'
);
// "functions" directory has output Functions
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual(['withTrailingSlash.func']);
});
it('should store `detectBuilders()` error in `builds.json`', async () => {
const cwd = fixture('error-vercel-json-validation');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(1);
// Error gets printed to the terminal
await expect(client.stderr).toOutput(
'Error: Function must contain at least one property.'
);
// `builds.json` contains top-level "error" property
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds.builds).toBeUndefined();
expect(builds.error.code).toEqual('invalid_function');
expect(builds.error.message).toEqual(
'Function must contain at least one property.'
);
// `config.json` contains `version`
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson.version).toBe(3);
});
it('should store Builder error in `builds.json`', async () => {
const cwd = fixture('node-error');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(1);
// Error gets printed to the terminal
await expect(client.stderr).toOutput("Duplicate identifier 'res'.");
// `builds.json` contains "error" build
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds.builds).toHaveLength(4);
const errorBuilds = builds.builds.filter((b: any) => 'error' in b);
expect(errorBuilds).toHaveLength(1);
expect(errorBuilds[0].error).toEqual({
name: 'Error',
message: expect.stringContaining('TS1005'),
stack: expect.stringContaining('api/typescript.ts'),
hideStackTrace: true,
code: 'NODE_TYPESCRIPT_ERROR',
});
// top level "error" also contains the same error
expect(builds.error).toEqual({
name: 'Error',
message: expect.stringContaining('TS1005'),
stack: expect.stringContaining('api/typescript.ts'),
hideStackTrace: true,
code: 'NODE_TYPESCRIPT_ERROR',
});
// `config.json` contains `version`
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson.version).toBe(3);
});
it('should error when "functions" has runtime that emits discontinued "nodejs12.x"', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on Windows');
return;
}
const cwd = fixture('discontinued-nodejs12.x');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(1);
// Error gets printed to the terminal
await expect(client.stderr).toOutput(
'The Runtime "vercel-php@0.1.0" is using "nodejs12.x", which is discontinued. Please upgrade your Runtime to a more recent version or consult the author for more details.'
);
// `builds.json` contains "error" build
const builds = await fs.readJSON(join(output, 'builds.json'));
const errorBuilds = builds.builds.filter((b: any) => 'error' in b);
expect(errorBuilds).toHaveLength(1);
expect(errorBuilds[0].error).toEqual({
name: 'Error',
message: expect.stringContaining('Please upgrade your Runtime'),
stack: expect.stringContaining('Please upgrade your Runtime'),
hideStackTrace: true,
code: 'NODEJS_DISCONTINUED_VERSION',
link: 'https://github.com/vercel/vercel/blob/main/DEVELOPING_A_RUNTIME.md#lambdaruntime',
});
// top level "error" also contains the same error
expect(builds.error).toEqual({
name: 'Error',
message: expect.stringContaining('Please upgrade your Runtime'),
stack: expect.stringContaining('Please upgrade your Runtime'),
hideStackTrace: true,
code: 'NODEJS_DISCONTINUED_VERSION',
link: 'https://github.com/vercel/vercel/blob/main/DEVELOPING_A_RUNTIME.md#lambdaruntime',
});
// `config.json` contains `version`
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson.version).toBe(3);
});
it('should allow for missing "build" script', async () => {
const cwd = fixture('static-with-pkg');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static',
apiVersion: 2,
src: '**',
use: '@vercel/static',
},
],
});
// "static" directory contains static files
const files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.html', 'package.json']);
});
it('should set `VERCEL_ANALYTICS_ID` environment variable and warn users', async () => {
const cwd = fixture('vercel-analytics');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const env = await fs.readJSON(join(output, 'static', 'env.json'));
expect(Object.keys(env).includes('VERCEL_ANALYTICS_ID')).toEqual(true);
await expect(client.stderr).toOutput(
'Vercel Speed Insights auto-injection is deprecated in favor of @vercel/speed-insights package. Learn more: https://vercel.link/upgrate-to-speed-insights-package'
);
});
it('should load environment variables from `.vercel/.env.preview.local`', async () => {
const cwd = fixture('env-from-vc-pull');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const env = await fs.readJSON(join(output, 'static', 'env.json'));
expect(env['ENV_FILE']).toEqual('preview');
});
it('should load environment variables from `.vercel/.env.production.local`', async () => {
const cwd = fixture('env-from-vc-pull');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
client.setArgv('build', '--prod');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const env = await fs.readJSON(join(output, 'static', 'env.json'));
expect(env['ENV_FILE']).toEqual('production');
});
it('should NOT load environment variables from `.env`', async () => {
const cwd = fixture('env-root-level');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const env = await fs.readJSON(join(output, 'static', 'env.json'));
// The `.env` in this fixture has `ENV_FILE=root"`,
// so if that's not defined then we're good
expect(env['ENV_FILE']).toBeUndefined();
});
it('should apply function configuration from "vercel.json" to Serverless Functions', async () => {
const cwd = fixture('lambda-with-128-memory');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// "functions/api" directory has output Functions
const functions = await fs.readdir(join(output, 'functions/api'));
expect(functions.sort()).toEqual(['memory.func']);
const vcConfig = await fs.readJSON(
join(output, 'functions/api/memory.func/.vc-config.json')
);
expect(vcConfig).toMatchObject({
handler: 'api/memory.js',
memory: 128,
environment: {},
launcherType: 'Nodejs',
shouldAddHelpers: true,
shouldAddSourcemapSupport: false,
awsLambdaHandler: '',
});
});
it('should apply project settings overrides from "vercel.json"', async () => {
if (process.platform === 'win32') {
// this test runs a build command with `mkdir -p` which is unsupported on Windows
console.log('Skipping test on Windows');
return;
}
const cwd = fixture('project-settings-override');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// The `buildCommand` override in "vercel.json" outputs "3" to the
// index.txt file, so verify that that was produced in the build output
const contents = await fs.readFile(
join(output, 'static/index.txt'),
'utf8'
);
expect(contents.trim()).toEqual('3');
});
it('should set VERCEL_PROJECT_SETTINGS_ environment variables', async () => {
const cwd = fixture('project-settings-env-vars');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const contents = await fs.readJSON(join(output, 'static/env.json'));
expect(contents).toMatchObject({
VERCEL_PROJECT_SETTINGS_BUILD_COMMAND: `node build.cjs`,
VERCEL_PROJECT_SETTINGS_INSTALL_COMMAND: '',
VERCEL_PROJECT_SETTINGS_OUTPUT_DIRECTORY: 'out',
VERCEL_PROJECT_SETTINGS_NODE_VERSION: '18.x',
});
});
it('should apply "images" configuration from `vercel.json`', async () => {
const cwd = fixture('images');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `config.json` includes "images" from `vercel.json`
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson).toMatchObject({
images: {
sizes: [256, 384, 600, 1000],
domains: [],
minimumCacheTTL: 60,
formats: ['image/avif', 'image/webp'],
contentDispositionType: 'attachment',
},
});
});
it('should fail with invalid "rewrites" configuration from `vercel.json`', async () => {
const cwd = fixture('invalid-rewrites');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
'Error: Invalid vercel.json - `rewrites[2]` should NOT have additional property `src`. Did you mean `source`?' +
'\n' +
'View Documentation: https://vercel.com/docs/concepts/projects/project-configuration#rewrites'
);
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds.builds).toBeUndefined();
expect(builds.error).toEqual({
name: 'Error',
message:
'Invalid vercel.json - `rewrites[2]` should NOT have additional property `src`. Did you mean `source`?',
stack: expect.stringContaining('at validateConfig'),
hideStackTrace: true,
code: 'INVALID_VERCEL_CONFIG',
link: 'https://vercel.com/docs/concepts/projects/project-configuration#rewrites',
action: 'View Documentation',
});
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson.version).toBe(3);
});
it('should include crons property in build output', async () => {
const cwd = fixture('with-cron');
const output = join(cwd, '.vercel', 'output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toBe(0);
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toHaveProperty('crons', [
{
path: '/api/cron-job',
schedule: '0 0 * * *',
},
]);
});
it('should merge crons property from build output with vercel.json crons property', async () => {
const cwd = fixture('with-cron-merge');
const output = join(cwd, '.vercel', 'output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toBe(0);
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toHaveProperty('crons', [
{
path: '/api/cron-job',
schedule: '0 0 * * *',
},
{
path: '/api/cron-job-build-output',
schedule: '0 0 * * *',
},
]);
});
describe('should find packages with different main/module/browser keys', function () {
let output: string;
beforeAll(async function () {
delete process.env.__VERCEL_BUILD_RUNNING;
const cwd = fixture('import-from-main-keys');
output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const functions = await fs.readdir(join(output, 'functions/api'));
const sortedFunctions = functions.sort();
expect(sortedFunctions).toEqual([
'prefer-browser.func',
'prefer-main.func',
'prefer-module.func',
'use-browser.func',
'use-classic.func',
'use-main.func',
'use-module.func',
]);
});
it('use-classic', async function () {
const packageDir = join(
output,
'functions/api',
'use-classic.func',
'packages',
'only-classic'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('index.js');
});
it('use-main', async function () {
const packageDir = join(
output,
'functions/api',
'use-main.func',
'packages',
'only-main'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-main.js');
});
it('use-module', async function () {
const packageDir = join(
output,
'functions/api',
'use-module.func',
'packages',
'only-module'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-module.js');
});
it('use-browser', async function () {
const packageDir = join(
output,
'functions/api',
'use-browser.func',
'packages',
'only-browser'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-browser.js');
});
it('prefer-browser', async function () {
const packageDir = join(
output,
'functions/api',
'prefer-browser.func',
'packages',
'prefer-browser'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-browser.js');
});
it('prefer-main', async function () {
const packageDir = join(
output,
'functions/api',
'prefer-main.func',
'packages',
'prefer-main'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-main.js');
});
it('prefer-module', async function () {
const packageDir = join(
output,
'functions/api',
'prefer-module.func',
'packages',
'prefer-module'
);
const packageDistFiles = await fs.readdir(packageDir);
expect(packageDistFiles).toContain('dist-module.js');
});
});
it('should use --local-config over default vercel.json', async () => {
const cwd = fixture('local-config');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
let exitCode = await build(client);
delete process.env.__VERCEL_BUILD_RUNNING;
expect(exitCode).toEqual(0);
let config = await fs.readJSON(join(output, 'config.json'));
expect(config.routes).toContainEqual({
src: '^/another-main$',
dest: '/main.html',
});
expect(config.routes).not.toContainEqual({
src: '^/another-test$',
dest: '/test.html',
});
client.localConfigPath = 'vercel-test.json';
exitCode = await build(client);
expect(exitCode).toEqual(0);
config = await fs.readJSON(join(output, 'config.json'));
expect(config.routes).not.toContainEqual({
src: '^/another-main$',
dest: '/main.html',
});
expect(config.routes).toContainEqual({
src: '^/another-test$',
dest: '/test.html',
});
});
it('should build Storybook project and ignore middleware', async () => {
const cwd = fixture('storybook-with-middleware');
const output = join(cwd, '.vercel/output');
try {
client.cwd = cwd;
process.env.STORYBOOK_DISABLE_TELEMETRY = '1';
execSync('npm install');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// `builds.json` says that "@vercel/static" was run
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds).toMatchObject({
target: 'preview',
builds: [
{
require: '@vercel/static-build',
apiVersion: 2,
src: 'package.json',
use: '@vercel/static-build',
},
],
});
const files = await fs.readdir(output);
// we should NOT see `functions` because that means `middleware.ts` was processed
expect(files.sort()).toEqual(['builds.json', 'config.json', 'static']);
} finally {
delete process.env.STORYBOOK_DISABLE_TELEMETRY;
}
});
it('should error if .npmrc exists containing use-node-version', async () => {
const cwd = fixture('npmrc-use-node-version');
client.cwd = cwd;
client.setArgv('build');
const exitCodePromise = build(client);
await expect(client.stderr).toOutput('Error: Detected unsupported');
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should ignore `.env` for static site', async () => {
const cwd = fixture('static-env');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
expect(fs.existsSync(join(output, 'static', 'index.html'))).toBe(true);
expect(fs.existsSync(join(output, 'static', '.env'))).toBe(false);
});
it('should build with `repo.json` link', async () => {
const cwd = fixture('../../monorepo-link');
useUser();
useTeams('team_dummy');
// "blog" app
useProject({
...defaultProject,
id: 'QmScb7GPQt6gsS',
name: 'monorepo-blog',
rootDirectory: 'blog',
outputDirectory: 'dist',
framework: null,
});
let output = join(cwd, 'blog/.vercel/output');
client.cwd = join(cwd, 'blog');
client.setArgv('build', '--yes');
let exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;
let files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('blog');
// "dashboard" app
useProject({
...defaultProject,
id: 'QmbKpqpiUqbcke',
name: 'monorepo-dashboard',
rootDirectory: 'dashboard',
outputDirectory: 'dist',
framework: null,
});
output = join(cwd, 'dashboard/.vercel/output');
client.cwd = join(cwd, 'dashboard');
client.setArgv('build', '--yes');
exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;
files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('dashboard');
// "marketing" app
useProject({
...defaultProject,
id: 'QmX6P93ChNDoZP',
name: 'monorepo-marketing',
rootDirectory: 'marketing',
outputDirectory: 'dist',
framework: null,
});
output = join(cwd, 'marketing/.vercel/output');
client.cwd = join(cwd, 'marketing');
client.setArgv('build', '--yes');
exitCode = await build(client);
expect(exitCode).toEqual(0);
delete process.env.__VERCEL_BUILD_RUNNING;
files = await fs.readdir(join(output, 'static'));
expect(files.sort()).toEqual(['index.txt']);
expect(
(await fs.readFile(join(output, 'static/index.txt'), 'utf8')).trim()
).toEqual('marketing');
});
it('should write to flags.json', async () => {
const cwd = fixture('with-flags');
const output = join(cwd, '.vercel', 'output');
client.cwd = cwd;
client.setArgv('build', '--yes');
const exitCode = await build(client);
expect(exitCode).toEqual(0);
expect(fs.existsSync(join(output, 'flags.json'))).toBe(true);
expect(fs.readJSONSync(join(output, 'flags.json'))).toEqual({
definitions: {
'my-next-flag': {
options: [{ value: true }, { value: false }],
},
},
});
});
it('should detect framework version in monorepo app', async () => {
const cwd = fixture('monorepo');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
const config = await fs.readJSON(join(output, 'config.json'));
expect(typeof config.framework.version).toEqual('string');
});
it('should create symlinks for duplicate references to Lambda / EdgeFunction instances', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on Windows');
return;
}
const cwd = fixture('functions-symlink');
const output = join(cwd, '.vercel/output');
client.cwd = cwd;
const exitCode = await build(client);
expect(exitCode).toEqual(0);
// "functions" directory has output Functions
const functions = await fs.readdir(join(output, 'functions'));
expect(functions.sort()).toEqual([
'edge.func',
'edge2.func',
'lambda.func',
'lambda2.func',
]);
expect(
fs.lstatSync(join(output, 'functions/lambda.func')).isDirectory()
).toEqual(true);
expect(
fs.lstatSync(join(output, 'functions/edge.func')).isDirectory()
).toEqual(true);
expect(
fs.lstatSync(join(output, 'functions/lambda2.func')).isSymbolicLink()
).toEqual(true);
expect(
fs.lstatSync(join(output, 'functions/edge2.func')).isSymbolicLink()
).toEqual(true);
expect(fs.readlinkSync(join(output, 'functions/lambda2.func'))).toEqual(
'lambda.func'
);
expect(fs.readlinkSync(join(output, 'functions/edge2.func'))).toEqual(
'edge.func'
);
});
});