mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 04:22:01 +00:00
[cli] Remove support for single file deployments (#6652)
Deploying a single file has printed a deprecation warning for a long time. Let's finally remove that behavior.
This commit is contained in:
9
errors/no-single-file-deployments.md
Normal file
9
errors/no-single-file-deployments.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# No Single File Deployments
|
||||||
|
|
||||||
|
#### Why This Error Occurred
|
||||||
|
|
||||||
|
You attempted to create a Vercel deployment where the input is a file, rather than a directory. Previously this was allowed, however this behavior has been removed as of Vercel CLI v24.0.0 because it exposed a potential security risk if the user accidentally created a deployment from a sensitive file.
|
||||||
|
|
||||||
|
#### Possible Ways to Fix It
|
||||||
|
|
||||||
|
- Run the `vercel deploy` command against a directory, instead of a file.
|
||||||
@@ -166,8 +166,8 @@ export default async (client: Client) => {
|
|||||||
return pathValidation.exitCode;
|
return pathValidation.exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isFile, path } = pathValidation;
|
const { path } = pathValidation;
|
||||||
const autoConfirm = argv['--confirm'] || isFile;
|
const autoConfirm = argv['--confirm'];
|
||||||
|
|
||||||
// deprecate --name
|
// deprecate --name
|
||||||
if (argv['--name']) {
|
if (argv['--name']) {
|
||||||
@@ -229,8 +229,7 @@ export default async (client: Client) => {
|
|||||||
// user input.
|
// user input.
|
||||||
const detectedProjectName = getProjectName({
|
const detectedProjectName = getProjectName({
|
||||||
argv,
|
argv,
|
||||||
nowConfig: localConfig || {},
|
nowConfig: localConfig,
|
||||||
isFile,
|
|
||||||
paths,
|
paths,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,7 +449,6 @@ export default async (client: Client) => {
|
|||||||
rootDirectory,
|
rootDirectory,
|
||||||
quiet,
|
quiet,
|
||||||
wantsPublic: argv['--public'] || localConfig.public,
|
wantsPublic: argv['--public'] || localConfig.public,
|
||||||
isFile,
|
|
||||||
type: null,
|
type: null,
|
||||||
nowConfig: localConfig,
|
nowConfig: localConfig,
|
||||||
regions,
|
regions,
|
||||||
@@ -472,7 +470,7 @@ export default async (client: Client) => {
|
|||||||
[sourcePath],
|
[sourcePath],
|
||||||
createArgs,
|
createArgs,
|
||||||
org,
|
org,
|
||||||
!project && !isFile,
|
!project,
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -655,8 +653,7 @@ export default async (client: Client) => {
|
|||||||
client,
|
client,
|
||||||
deployment,
|
deployment,
|
||||||
deployStamp,
|
deployStamp,
|
||||||
!argv['--no-clipboard'],
|
!argv['--no-clipboard']
|
||||||
isFile
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -791,8 +788,7 @@ const printDeploymentStatus = async (
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
deployStamp: () => string,
|
deployStamp: () => string,
|
||||||
isClipboardEnabled: boolean,
|
isClipboardEnabled: boolean
|
||||||
isFile: boolean
|
|
||||||
) => {
|
) => {
|
||||||
indications = indications || [];
|
indications = indications || [];
|
||||||
const isProdDeployment = target === 'production';
|
const isProdDeployment = target === 'production';
|
||||||
@@ -814,7 +810,7 @@ const printDeploymentStatus = async (
|
|||||||
// print preview/production url
|
// print preview/production url
|
||||||
let previewUrl: string;
|
let previewUrl: string;
|
||||||
let isWildcard: boolean;
|
let isWildcard: boolean;
|
||||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
if (Array.isArray(aliasList) && aliasList.length > 0) {
|
||||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||||
if (previewUrlInfo) {
|
if (previewUrlInfo) {
|
||||||
isWildcard = previewUrlInfo.isWildcard;
|
isWildcard = previewUrlInfo.isWildcard;
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { VercelConfig } from '@vercel/client';
|
|||||||
export interface GetProjectNameOptions {
|
export interface GetProjectNameOptions {
|
||||||
argv: { '--name'?: string };
|
argv: { '--name'?: string };
|
||||||
nowConfig?: VercelConfig;
|
nowConfig?: VercelConfig;
|
||||||
isFile?: boolean;
|
|
||||||
paths?: string[];
|
paths?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function getProjectName({
|
export default function getProjectName({
|
||||||
argv,
|
argv,
|
||||||
nowConfig = {},
|
nowConfig = {},
|
||||||
isFile = false,
|
|
||||||
paths = [],
|
paths = [],
|
||||||
}: GetProjectNameOptions) {
|
}: GetProjectNameOptions) {
|
||||||
const nameCli = argv['--name'];
|
const nameCli = argv['--name'];
|
||||||
@@ -24,10 +22,6 @@ export default function getProjectName({
|
|||||||
return nowConfig.name;
|
return nowConfig.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFile || paths.length > 1) {
|
|
||||||
return 'files';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use the name of the directory
|
// Otherwise, use the name of the directory
|
||||||
return basename(paths[0] || '');
|
return basename(paths[0] || '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export interface NowOptions {
|
|||||||
export interface CreateOptions {
|
export interface CreateOptions {
|
||||||
// Legacy
|
// Legacy
|
||||||
nowConfig?: VercelConfig;
|
nowConfig?: VercelConfig;
|
||||||
isFile?: boolean;
|
|
||||||
|
|
||||||
// Latest
|
// Latest
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default async function inputProject(
|
|||||||
client: Client,
|
client: Client,
|
||||||
org: Org,
|
org: Org,
|
||||||
detectedProjectName: string,
|
detectedProjectName: string,
|
||||||
autoConfirm: boolean
|
autoConfirm = false
|
||||||
): Promise<Project | string> {
|
): Promise<Project | string> {
|
||||||
const { output } = client;
|
const { output } = client;
|
||||||
const slugifiedName = slugify(detectedProjectName);
|
const slugifiedName = slugify(detectedProjectName);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { validateRootDirectory } from '../validate-paths';
|
|||||||
export async function inputRootDirectory(
|
export async function inputRootDirectory(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
output: Output,
|
output: Output,
|
||||||
autoConfirm: boolean
|
autoConfirm = false
|
||||||
) {
|
) {
|
||||||
if (autoConfirm) {
|
if (autoConfirm) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export default async function setupAndLink(
|
|||||||
withCache: undefined,
|
withCache: undefined,
|
||||||
quiet,
|
quiet,
|
||||||
wantsPublic: localConfig?.public || false,
|
wantsPublic: localConfig?.public || false,
|
||||||
isFile,
|
|
||||||
nowConfig: localConfig,
|
nowConfig: localConfig,
|
||||||
regions: undefined,
|
regions: undefined,
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -179,7 +178,7 @@ export default async function setupAndLink(
|
|||||||
[sourcePath],
|
[sourcePath],
|
||||||
createArgs,
|
createArgs,
|
||||||
org,
|
org,
|
||||||
!isFile,
|
true,
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Output } from './output';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import confirm from './input/confirm';
|
import confirm from './input/confirm';
|
||||||
import { prependEmoji, emoji } from './emoji';
|
|
||||||
import toHumanPath from './humanize-path';
|
import toHumanPath from './humanize-path';
|
||||||
|
|
||||||
const stat = promisify(lstatRaw);
|
const stat = promisify(lstatRaw);
|
||||||
@@ -54,10 +53,7 @@ export async function validateRootDirectory(
|
|||||||
export default async function validatePaths(
|
export default async function validatePaths(
|
||||||
output: Output,
|
output: Output,
|
||||||
paths: string[]
|
paths: string[]
|
||||||
): Promise<
|
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
|
||||||
| { valid: true; path: string; isFile: boolean }
|
|
||||||
| { valid: false; exitCode: number }
|
|
||||||
> {
|
|
||||||
// can't deploy more than 1 path
|
// can't deploy more than 1 path
|
||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
|
output.print(`${chalk.red('Error!')} Can't deploy more than one path.\n`);
|
||||||
@@ -78,14 +74,12 @@ export default async function validatePaths(
|
|||||||
return { valid: false, exitCode: 1 };
|
return { valid: false, exitCode: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFile = pathStat && !pathStat.isDirectory();
|
if (!pathStat.isDirectory()) {
|
||||||
if (isFile) {
|
output.prettyError({
|
||||||
output.print(
|
message: 'Support for single file deployments has been removed.',
|
||||||
`${prependEmoji(
|
link: 'https://vercel.link/no-single-file-deployments',
|
||||||
'Deploying files with Vercel is deprecated (https://vercel.link/faq-deploy-file)',
|
});
|
||||||
emoji('warning')
|
return { valid: false, exitCode: 1 };
|
||||||
)}\n`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask confirmation if the directory is home
|
// ask confirmation if the directory is home
|
||||||
@@ -101,5 +95,5 @@ export default async function validatePaths(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, path, isFile };
|
return { valid: true, path };
|
||||||
}
|
}
|
||||||
|
|||||||
60
packages/cli/test/commands/deploy.test.ts
Normal file
60
packages/cli/test/commands/deploy.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { fileNameSymbol } from '@vercel/client';
|
||||||
|
import { client } from '../mocks/client';
|
||||||
|
import deploy from '../../src/commands/deploy';
|
||||||
|
|
||||||
|
describe('deploy', () => {
|
||||||
|
it('should reject deploying a single file', async () => {
|
||||||
|
client.setArgv('deploy', __filename);
|
||||||
|
const exitCode = await deploy(client);
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
expect(client.outputBuffer).toEqual(
|
||||||
|
`Error! Support for single file deployments has been removed.\nLearn More: https://vercel.link/no-single-file-deployments\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject deploying multiple files', async () => {
|
||||||
|
client.setArgv('deploy', __filename, join(__dirname, 'inspect.test.ts'));
|
||||||
|
const exitCode = await deploy(client);
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
expect(client.outputBuffer).toEqual(
|
||||||
|
`Error! Can't deploy more than one path.\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject deploying a directory that does not exist', async () => {
|
||||||
|
client.setArgv('deploy', 'does-not-exists');
|
||||||
|
const exitCode = await deploy(client);
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
expect(client.outputBuffer).toEqual(
|
||||||
|
`Error! The specified file or directory "does-not-exists" does not exist.\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject deploying "version: 1"', async () => {
|
||||||
|
client.setArgv('deploy');
|
||||||
|
client.localConfig = {
|
||||||
|
[fileNameSymbol]: 'vercel.json',
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
const exitCode = await deploy(client);
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
expect(client.outputBuffer).toEqual(
|
||||||
|
'Error! The value of the `version` property within vercel.json can only be `2`.\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject deploying "version: {}"', async () => {
|
||||||
|
client.setArgv('deploy');
|
||||||
|
client.localConfig = {
|
||||||
|
[fileNameSymbol]: 'vercel.json',
|
||||||
|
// @ts-ignore
|
||||||
|
version: {},
|
||||||
|
};
|
||||||
|
const exitCode = await deploy(client);
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
expect(client.outputBuffer).toEqual(
|
||||||
|
'Error! The `version` property inside your vercel.json file must be a number.\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
53
packages/cli/test/integration.js
vendored
53
packages/cli/test/integration.js
vendored
@@ -1913,59 +1913,6 @@ test('create a production deployment', async t => {
|
|||||||
t.is(deployment.target, 'production', JSON.stringify(deployment, null, 2));
|
t.is(deployment.target, 'production', JSON.stringify(deployment, null, 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deploying a file should not show prompts and display deprecation', async t => {
|
|
||||||
const file = fixture('static-single-file/first.png');
|
|
||||||
|
|
||||||
const output = await execute([file], {
|
|
||||||
reject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { stdout, stderr, exitCode } = output;
|
|
||||||
|
|
||||||
// Ensure the exit code is right
|
|
||||||
t.is(exitCode, 0, formatOutput(output));
|
|
||||||
t.true(stderr.includes('Deploying files with Vercel is deprecated'));
|
|
||||||
|
|
||||||
// Ensure `.vercel` was not created
|
|
||||||
t.is(
|
|
||||||
await exists(path.join(path.dirname(file), '.vercel')),
|
|
||||||
false,
|
|
||||||
'.vercel should not exists'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test if the output is really a URL
|
|
||||||
const { href, host } = new URL(stdout);
|
|
||||||
t.is(host.split('-')[0], 'files');
|
|
||||||
|
|
||||||
// Send a test request to the deployment
|
|
||||||
const response = await fetch(href);
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
|
|
||||||
t.is(contentType, 'image/png');
|
|
||||||
t.deepEqual(await readFile(file), await response.buffer());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('deploying more than 1 path should fail', async t => {
|
|
||||||
const file1 = fixture('static-multiple-files/first.png');
|
|
||||||
const file2 = fixture('static-multiple-files/second.png');
|
|
||||||
|
|
||||||
const { stdout, stderr, exitCode } = await execa(
|
|
||||||
binaryPath,
|
|
||||||
[file1, file2, '--public', '--name', session, ...defaultArgs, '--confirm'],
|
|
||||||
{
|
|
||||||
reject: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(stderr);
|
|
||||||
console.log(stdout);
|
|
||||||
console.log(exitCode);
|
|
||||||
|
|
||||||
// Ensure the exit code is right
|
|
||||||
t.is(exitCode, 1);
|
|
||||||
t.true(stderr.trim().endsWith(`Can't deploy more than one path.`));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('use build-env', async t => {
|
test('use build-env', async t => {
|
||||||
const directory = fixture('build-env');
|
const directory = fixture('build-env');
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('getProjectName', () => {
|
|||||||
expect(project).toEqual('abc');
|
expect(project).toEqual('abc');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with now.json', () => {
|
it('should work with `vercel.json` config', () => {
|
||||||
const project = getProjectName({
|
const project = getProjectName({
|
||||||
argv: {},
|
argv: {},
|
||||||
nowConfig: { name: 'abc' },
|
nowConfig: { name: 'abc' },
|
||||||
@@ -18,24 +18,6 @@ describe('getProjectName', () => {
|
|||||||
expect(project).toEqual('abc');
|
expect(project).toEqual('abc');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with a file', () => {
|
|
||||||
const project = getProjectName({
|
|
||||||
argv: {},
|
|
||||||
nowConfig: {},
|
|
||||||
isFile: true,
|
|
||||||
});
|
|
||||||
expect(project).toEqual('files');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with a multiple files', () => {
|
|
||||||
const project = getProjectName({
|
|
||||||
argv: {},
|
|
||||||
nowConfig: {},
|
|
||||||
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
|
|
||||||
});
|
|
||||||
expect(project).toEqual('files');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with a directory', () => {
|
it('should work with a directory', () => {
|
||||||
const project = getProjectName({
|
const project = getProjectName({
|
||||||
argv: {},
|
argv: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user