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' ); }); });