import ms from 'ms'; import fs from 'fs-extra'; import { join } from 'path'; 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'; jest.setTimeout(ms('1 minute')); const fixture = (name: string) => join(__dirname, '../../fixtures/unit/commands/build', name); describe('build', () => { const originalCwd = process.cwd(); it('should build with `@vercel/static`', async () => { const cwd = fixture('static'); const output = join(cwd, '.vercel/output'); try { process.chdir(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']); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); it('should build with `@vercel/node`', async () => { const cwd = fixture('node'); const output = join(cwd, '.vercel/output'); try { process.chdir(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', ]); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); it('should normalize "src" path in `vercel.json`', async () => { const cwd = fixture('normalize-src'); const output = join(cwd, '.vercel/output'); try { process.chdir(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']); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); it('should build with 3rd party Builder', async () => { const cwd = fixture('third-party-builder'); const output = join(cwd, '.vercel/output'); try { process.chdir(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: '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: {}, }); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); it('should serialize `EdgeFunction` output in version 3 Builder', async () => { const cwd = fixture('edge-function'); const output = join(cwd, '.vercel/output'); try { process.chdir(cwd); client.setArgv('build', '--prod'); 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: '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', }); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); 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 { process.chdir(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 }); process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); 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 { process.chdir(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 }); process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); it('should build root-level `middleware.js` and exclude from static files', async () => { const cwd = fixture('middleware'); const output = join(cwd, '.vercel/output'); try { process.chdir(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', continue: true }, { handle: 'filesystem' }, { src: '^/api(/.*)?$', status: 404 }, { 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']); } finally { process.chdir(originalCwd); delete process.env.__VERCEL_BUILD_RUNNING; } }); });