mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
This PR:
- updates `packages/frameworks` to have most supported frameworks specify which dependency version should reflect the overall framework version
- updates `packages/fs-detectors` to allow framework detection that returns the full `Framework` record instead of just the slug
- updates `packages/next` to return the detected Next.js version in the build result
- updates `packages/cli` to leverage these changes so that `vc build` can add `framework: { version: string; }` to `config.json` output
The result is that Build Output API and supported frameworks will return their framework version in the build result of `vc build` when possible, which is used by the build container when creating the deployment. The dashboard later retrieves this value to display in richer deployment outputs.
Supports:
- https://github.com/vercel/api/pull/15601
- https://github.com/vercel/front/pull/18319
---
With the related build container updates, we get to see Next.js version in the build output. You'll see this with BOA+Prebuilt or a normal deploy:
<img width="1228" alt="Screen Shot 2022-12-09 at 2 48 12 PM" src="https://user-images.githubusercontent.com/41545/206793639-f9cd3bdf-b822-45dd-9564-95b94994271d.png">
---
### The Path to this PR
I went through all the supported frameworks and figured out how to best determine their versions. For most of them, we can check a known dependency's installed version number.
We can get most of the way only checking npm. For a handful, we'd have to support Go/Ruby/Rust/Whatever dependencies.
I started with a more complex method signature to allow for later expansion without changing the signature. It looked like this, in practice:
```
async getVersion(dependencies: DependencyMap) => depedencies['next']
```
However, after checking all currently supported frameworks, I don't think this will end up being necessary. It also has the constraint that all dependencies have to be gathered and presented to the function even though it only needs to check for one or two. That's not a huge deal if we have them already where we need them, but we don't. We could use a variant here where this function does its own lookups, but this seemed unnecessary and would beg for duplication and small variances that could cause bugs.
Further, if we only look at `package.json`, we're going to either see a specific version of a version range. To be precise, we have to look at the installed version of the package. That means checking one of the various types of lockfiles that can exist or poking into node_modules.
If we poke into node_modules to detect the installed version, we introduce another point where Yarn 3 (default mode) will not be supported. If we read lockfiles, we have to potentially parse `npm`, `pnpm`, and `yarn` lockfiles.
If we use `npm ls <package-name>`, that also fails in Yarn 3 (default mode). We could accept that and go forward anyway, which would look like:
```
const args = `ls ${packageName} --depth=0 --json`.split(' ');
const { stdout } = await execa('npm', args, { cwd });
const regex = new RegExp(String.raw`${packageName}@([\.\d]+)`);
const matches = stdout.match(regex);
if (matches) {
return matches[1];
}
```
But it turns out there's a `--json` option! That's what I ended up using, for now.
We could explore the lockfile route more, but after some initial digging, it' non-trivial. There are 3 main lockfiles we'd want to check for (npm, pnpm, and yarn) and there are different lockfile versions that put necessary data in different places. I looked for existing tools that parse this, but I didn't find any. We could certainly go down this path, but the effort doesn't seem worth it when `npm ls` gets us really close.
---
### Follow-up Versioning
Now that we know how to determine version per framework, we can vary configuration by version. In a future PR, we could allow a given value to vary by version number:
```
name: (version) => {
if (semver.gt(version, '9.8.7')) {
return 'some-framework-2''
}
return 'some-framework';
}
```
However, it may still be easier to differentiate significant versions by adding multiple entries in the list.
1560 lines
49 KiB
TypeScript
1560 lines
49 KiB
TypeScript
import ms from 'ms';
|
|
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 { setupFixture } from '../../../helpers/setup-fixture';
|
|
import JSON5 from 'json5';
|
|
// TODO (@Ethan-Arrowood) - After shipping support for turbo and nx, revisit rush support
|
|
// import execa from 'execa';
|
|
|
|
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 `@now/static`', async () => {
|
|
const cwd = fixture('now-static');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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']);
|
|
} 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 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;
|
|
}
|
|
|
|
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(['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);
|
|
} finally {
|
|
await fs.unlink(join(cwd, 'foo.html'));
|
|
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 "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: {},
|
|
});
|
|
} 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 "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',
|
|
});
|
|
} 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/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',
|
|
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']);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should build root-level `middleware.js` with "Root Directory" setting', async () => {
|
|
const cwd = fixture('middleware-root-directory');
|
|
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',
|
|
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']);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should build root-level `middleware.js` with "matcher" config', async () => {
|
|
const cwd = fixture('middleware-with-matcher');
|
|
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: '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',
|
|
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']);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should support `--output` parameter', async () => {
|
|
const cwd = fixture('static');
|
|
const output = await getWriteableDirectory();
|
|
try {
|
|
process.chdir(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);
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
// 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');
|
|
try {
|
|
process.chdir(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']);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should store `detectBuilders()` error in `builds.json`', async () => {
|
|
const cwd = fixture('error-vercel-json-validation');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should store Builder error in `builds.json`', async () => {
|
|
const cwd = fixture('node-error');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
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');
|
|
try {
|
|
process.chdir(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);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should allow for missing "build" script', async () => {
|
|
const cwd = fixture('static-with-pkg');
|
|
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', 'package.json']);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should set `VERCEL_ANALYTICS_ID` environment variable', async () => {
|
|
const cwd = fixture('vercel-analytics');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should load environment variables from `.vercel/.env.preview.local`', async () => {
|
|
const cwd = fixture('env-from-vc-pull');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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');
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should load environment variables from `.vercel/.env.production.local`', async () => {
|
|
const cwd = fixture('env-from-vc-pull');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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');
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should NOT load environment variables from `.env`', async () => {
|
|
const cwd = fixture('env-root-level');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
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');
|
|
try {
|
|
process.chdir(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: '',
|
|
});
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
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');
|
|
try {
|
|
process.chdir(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');
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should apply "images" configuration from `vercel.json`', async () => {
|
|
const cwd = fixture('images');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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'],
|
|
},
|
|
});
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
it('should fail with invalid "rewrites" configuration from `vercel.json`', async () => {
|
|
const cwd = fixture('invalid-rewrites');
|
|
const output = join(cwd, '.vercel/output');
|
|
try {
|
|
process.chdir(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/configuration#project/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/configuration#project/rewrites',
|
|
action: 'View Documentation',
|
|
});
|
|
const configJson = await fs.readJSON(join(output, 'config.json'));
|
|
expect(configJson.version).toBe(3);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
});
|
|
|
|
describe('should find packages with different main/module/browser keys', function () {
|
|
let output: string;
|
|
|
|
beforeAll(async function () {
|
|
const cwd = fixture('import-from-main-keys');
|
|
output = join(cwd, '.vercel/output');
|
|
|
|
process.chdir(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',
|
|
]);
|
|
});
|
|
|
|
afterAll(function () {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* TODO (@Ethan-Arrowood) - After shipping support for turbo and nx, revisit rush support
|
|
* Previously, rush tests were too flaky and consistently would timeout beyone the excessive
|
|
* timeout window granted for these tests. Maybe we are doing something wrong.
|
|
*/
|
|
describe('monorepo-detection', () => {
|
|
beforeAll(() => {
|
|
process.env.VERCEL_BUILD_MONOREPO_SUPPORT = '1';
|
|
});
|
|
|
|
afterAll(() => {
|
|
delete process.env.VERCEL_BUILD_MONOREPO_SUPPORT;
|
|
});
|
|
|
|
const setupMonorepoDetectionFixture = (fixture: string) => {
|
|
const cwd = setupFixture(`commands/build/monorepo-detection/${fixture}`);
|
|
process.chdir(cwd);
|
|
return cwd;
|
|
};
|
|
|
|
describe.each([
|
|
'nx',
|
|
'nx-package-config',
|
|
'nx-project-and-package-config-1',
|
|
'nx-project-and-package-config-2',
|
|
'nx-project-config',
|
|
// 'rush',
|
|
'turbo',
|
|
'turbo-package-config',
|
|
])('fixture: %s', fixture => {
|
|
const monorepoManagerMap: Record<string, Record<string, string>> = {
|
|
turbo: {
|
|
name: 'Turbo',
|
|
buildCommand: 'cd ../.. && npx turbo run build --filter=app-1...',
|
|
installCommand: 'cd ../.. && yarn install',
|
|
ignoreCommand: 'cd ../.. && npx turbo-ignore',
|
|
},
|
|
nx: {
|
|
name: 'Nx',
|
|
buildCommand: 'cd ../.. && npx nx build app-1',
|
|
installCommand: 'cd ../.. && yarn install',
|
|
},
|
|
// rush: {
|
|
// name: 'Rush',
|
|
// buildCommand:
|
|
// 'node ../../common/scripts/install-run-rush.js build --to app-1',
|
|
// installCommand:
|
|
// 'node ../../common/scripts/install-run-rush.js install',
|
|
// },
|
|
};
|
|
|
|
const { name, ...commands } = monorepoManagerMap[fixture.split('-')[0]];
|
|
|
|
test(
|
|
'should detect and use correct defaults',
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
// if (fixture === 'rush') {
|
|
// await execa('npx', ['@microsoft/rush', 'update'], {
|
|
// cwd,
|
|
// reject: false,
|
|
// });
|
|
// }
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
await expect(client.stderr).toOutput(
|
|
`Automatically detected ${name} monorepo manager. Attempting to assign default settings.`
|
|
);
|
|
const result = await fs.readFile(
|
|
join(cwd, '.vercel/output/static/index.txt'),
|
|
'utf8'
|
|
);
|
|
expect(result).toMatch(/Hello, world/);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('5 minutes')
|
|
);
|
|
|
|
test(
|
|
'should not override preconfigured project settings',
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
// if (fixture === 'rush') {
|
|
// await execa('npx', ['@microsoft/rush', 'update'], {
|
|
// cwd,
|
|
// reject: false,
|
|
// });
|
|
// }
|
|
|
|
const projectJSONPath = join(cwd, '.vercel/project.json');
|
|
const projectJSON = JSON.parse(
|
|
await fs.readFile(projectJSONPath, 'utf-8')
|
|
);
|
|
|
|
const projectJSONCommands = {
|
|
...commands,
|
|
};
|
|
|
|
if (projectJSONCommands.ignoreCommand) {
|
|
projectJSONCommands.commandForIgnoringBuildStep =
|
|
projectJSONCommands.ignoreCommand;
|
|
delete projectJSONCommands.ignoreCommand;
|
|
}
|
|
|
|
await fs.writeFile(
|
|
projectJSONPath,
|
|
JSON.stringify({
|
|
...projectJSON,
|
|
settings: {
|
|
...projectJSON.settings,
|
|
...projectJSONCommands,
|
|
},
|
|
})
|
|
);
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign buildCommand as it is already set via project settings or configuration overrides.'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign installCommand as it is already set via project settings or configuration overrides.'
|
|
);
|
|
if (name === 'Turbo') {
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign commandForIgnoringBuildStep as it is already set via project settings or configuration overrides.'
|
|
);
|
|
}
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('5 minutes')
|
|
);
|
|
|
|
test(
|
|
'should not override configuration overrides',
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
// if (fixture === 'rush') {
|
|
// await execa('npx', ['@microsoft/rush', 'update'], {
|
|
// cwd,
|
|
// reject: false,
|
|
// });
|
|
// }
|
|
|
|
await fs.writeFile(
|
|
join(cwd, 'packages/app-1/vercel.json'),
|
|
JSON.stringify({
|
|
...commands,
|
|
})
|
|
);
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign buildCommand as it is already set via project settings or configuration overrides.'
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign installCommand as it is already set via project settings or configuration overrides.'
|
|
);
|
|
if (name === 'Turbo') {
|
|
await expect(client.stderr).toOutput(
|
|
'Cannot automatically assign commandForIgnoringBuildStep as it is already set via project settings or configuration overrides.'
|
|
);
|
|
}
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('5 minutes')
|
|
);
|
|
|
|
test(
|
|
`skip when rootDirectory is null or '.'`,
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
const projectJSONPath = join(cwd, '.vercel/project.json');
|
|
const projectJSON = JSON.parse(
|
|
await fs.readFile(projectJSONPath, 'utf-8')
|
|
);
|
|
|
|
await fs.writeFile(
|
|
projectJSONPath,
|
|
JSON.stringify({
|
|
...projectJSON,
|
|
settings: {
|
|
rootDirectory: null,
|
|
outputDirectory: null,
|
|
},
|
|
})
|
|
);
|
|
|
|
const packageJSONPath = join(cwd, 'package.json');
|
|
const packageJSON = JSON.parse(
|
|
await fs.readFile(packageJSONPath, 'utf-8')
|
|
);
|
|
|
|
await fs.writeFile(
|
|
packageJSONPath,
|
|
JSON.stringify({
|
|
...packageJSON,
|
|
scripts: {
|
|
...packageJSON.scripts,
|
|
build: `node build.js`,
|
|
},
|
|
})
|
|
);
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
const result = await fs.readFile(
|
|
join(cwd, '.vercel/output/static/index.txt'),
|
|
'utf8'
|
|
);
|
|
expect(result).toMatch(/Hello, from build\.js/);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('5 miuntes')
|
|
);
|
|
|
|
test(
|
|
`skip when vercel-build is defined`,
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
const packageJSONPath = join(cwd, 'packages/app-1/package.json');
|
|
const packageJSON = JSON.parse(
|
|
await fs.readFile(packageJSONPath, 'utf-8')
|
|
);
|
|
|
|
await fs.writeFile(
|
|
packageJSONPath,
|
|
JSON.stringify({
|
|
...packageJSON,
|
|
scripts: {
|
|
...packageJSON.scripts,
|
|
'vercel-build': `node ../../build.js`,
|
|
},
|
|
})
|
|
);
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
const result = await fs.readFile(
|
|
join(cwd, '.vercel/output/static/index.txt'),
|
|
'utf8'
|
|
);
|
|
expect(result).toMatch(/Hello, from build\.js/);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('5 miuntes')
|
|
);
|
|
|
|
test(
|
|
`skip when VERCEL_BUILD_MONOREPO_SUPPORT is disabled`,
|
|
async () => {
|
|
try {
|
|
process.env.VERCEL_BUILD_MONOREPO_SUPPORT = '0';
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
const packageJSONPath = join(cwd, 'packages/app-1/package.json');
|
|
const packageJSON = JSON.parse(
|
|
await fs.readFile(packageJSONPath, 'utf-8')
|
|
);
|
|
|
|
await fs.writeFile(
|
|
packageJSONPath,
|
|
JSON.stringify({
|
|
...packageJSON,
|
|
scripts: {
|
|
...packageJSON.scripts,
|
|
build: `node ../../build.js`,
|
|
},
|
|
})
|
|
);
|
|
|
|
const exitCode = await build(client);
|
|
expect(exitCode).toBe(0);
|
|
const result = await fs.readFile(
|
|
join(cwd, '.vercel/output/static/index.txt'),
|
|
'utf8'
|
|
);
|
|
expect(result).toMatch(/Hello, from build\.js/);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
process.env.VERCEL_BUILD_MONOREPO_SUPPORT = '1';
|
|
}
|
|
},
|
|
ms('5 miuntes')
|
|
);
|
|
});
|
|
|
|
describe.each([
|
|
[
|
|
'nx',
|
|
'nx.json',
|
|
'targetDefaults.build',
|
|
[
|
|
'Missing required `build` target in either nx.json, project.json, or package.json Nx configuration. Skipping automatic setting assignment.',
|
|
],
|
|
],
|
|
[
|
|
'nx-project-config',
|
|
'packages/app-1/project.json',
|
|
'targets.build',
|
|
[
|
|
'Missing required `build` target in either nx.json, project.json, or package.json Nx configuration. Skipping automatic setting assignment.',
|
|
],
|
|
],
|
|
[
|
|
'nx-package-config',
|
|
'packages/app-1/package.json',
|
|
'nx.targets.build',
|
|
[
|
|
'Missing required `build` target in either nx.json, project.json, or package.json Nx configuration. Skipping automatic setting assignment.',
|
|
],
|
|
],
|
|
[
|
|
'turbo',
|
|
'turbo.json',
|
|
'pipeline.build',
|
|
[
|
|
'Missing required `build` pipeline in turbo.json or package.json Turbo configuration. Skipping automatic setting assignment.',
|
|
],
|
|
],
|
|
[
|
|
'turbo-package-config',
|
|
'package.json',
|
|
'turbo.pipeline.build',
|
|
[
|
|
'Missing required `build` pipeline in turbo.json or package.json Turbo configuration. Skipping automatic setting assignment.',
|
|
],
|
|
],
|
|
])('fixture: %s', (fixture, configFile, propertyAccessor, expectedLogs) => {
|
|
function deleteSubProperty(
|
|
obj: { [k: string]: any },
|
|
accessorString: string
|
|
) {
|
|
const accessors = accessorString.split('.');
|
|
const lastAccessor = accessors.pop();
|
|
for (const accessor of accessors) {
|
|
obj = obj[accessor];
|
|
}
|
|
// lastAccessor cannot be undefined as accessors will always be an array of atleast one string
|
|
delete obj[lastAccessor as string];
|
|
}
|
|
|
|
test(
|
|
'should warn and not configure settings when project does not satisfy requirements',
|
|
async () => {
|
|
try {
|
|
const cwd = setupMonorepoDetectionFixture(fixture);
|
|
|
|
const configPath = join(cwd, configFile);
|
|
const config = JSON5.parse(await fs.readFile(configPath, 'utf-8'));
|
|
|
|
deleteSubProperty(config, propertyAccessor);
|
|
await fs.writeFile(configPath, JSON.stringify(config));
|
|
|
|
const exitCode = await build(client);
|
|
|
|
expect(exitCode).toBe(1);
|
|
for (const log of expectedLogs) {
|
|
await expect(client.stderr).toOutput(log);
|
|
}
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
delete process.env.__VERCEL_BUILD_RUNNING;
|
|
}
|
|
},
|
|
ms('3 minutes')
|
|
);
|
|
});
|
|
});
|
|
});
|