mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-23 01:49:13 +00:00
Compare commits
21 Commits
vercel-plu
...
vercel-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecb4481226 | ||
|
|
347407b244 | ||
|
|
01ea7b4b2a | ||
|
|
475a227ba9 | ||
|
|
40ca92f2e9 | ||
|
|
465129e62e | ||
|
|
bf0d5a7f29 | ||
|
|
d3ef240f6e | ||
|
|
5b26ebc7b8 | ||
|
|
3427ad6ce0 | ||
|
|
4ab5e4326b | ||
|
|
d24a3ce3ab | ||
|
|
29a44db8d9 | ||
|
|
695f3a9212 | ||
|
|
3ff777b8ed | ||
|
|
d94b9806ab | ||
|
|
35c8fc2729 | ||
|
|
0a468fd6d7 | ||
|
|
d31ebbabe4 | ||
|
|
09c9b71adb | ||
|
|
5975db4d66 |
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.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.12.3-canary.39",
|
||||
"version": "2.12.3-canary.44",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -86,10 +86,10 @@ export function convertRuntimeToPlugin(
|
||||
|
||||
const pages: { [key: string]: any } = {};
|
||||
const pluginName = packageName.replace('vercel-plugin-', '');
|
||||
const outputPath = join(workPath, '.output');
|
||||
|
||||
const traceDir = join(
|
||||
workPath,
|
||||
`.output`,
|
||||
outputPath,
|
||||
`inputs`,
|
||||
// Legacy Runtimes can only provide API Routes, so that's
|
||||
// why we can use this prefix for all of them. Here, we have to
|
||||
@@ -100,10 +100,7 @@ export function convertRuntimeToPlugin(
|
||||
|
||||
await fs.ensureDir(traceDir);
|
||||
|
||||
let newPathsRuntime: Set<string> = new Set();
|
||||
|
||||
const entryDir = join('.output', 'server', 'pages');
|
||||
const entryRoot = join(workPath, entryDir);
|
||||
const entryRoot = join(outputPath, 'server', 'pages');
|
||||
|
||||
for (const entrypoint of Object.keys(entrypoints)) {
|
||||
const { output } = await buildRuntime({
|
||||
@@ -119,17 +116,6 @@ export function convertRuntimeToPlugin(
|
||||
},
|
||||
});
|
||||
|
||||
// Legacy Runtimes tend to pollute the `workPath` with compiled results,
|
||||
// because the `workPath` used to be a place that was a place where they could
|
||||
// just put anything, but nowadays it's the working directory of the `vercel build`
|
||||
// command, which is the place where the developer keeps their source files,
|
||||
// so we don't want to pollute this space unnecessarily. That means we have to clean
|
||||
// up files that were created by the build, which is done further below.
|
||||
const sourceFilesAfterBuild = await getSourceFiles(
|
||||
workPath,
|
||||
ignoreFilter
|
||||
);
|
||||
|
||||
// @ts-ignore This symbol is a private API
|
||||
const lambdaFiles: Files = output[FILES_SYMBOL];
|
||||
|
||||
@@ -145,6 +131,7 @@ export function convertRuntimeToPlugin(
|
||||
|
||||
let handlerFileBase = output.handler;
|
||||
let handlerFile = lambdaFiles[handlerFileBase];
|
||||
let handlerHasImport = false;
|
||||
|
||||
const { handler } = output;
|
||||
const handlerMethod = handler.split('.').pop();
|
||||
@@ -158,6 +145,7 @@ export function convertRuntimeToPlugin(
|
||||
if (!handlerFile) {
|
||||
handlerFileBase = handlerFileName + ext;
|
||||
handlerFile = lambdaFiles[handlerFileBase];
|
||||
handlerHasImport = true;
|
||||
}
|
||||
|
||||
if (!handlerFile || !handlerFile.fsPath) {
|
||||
@@ -172,65 +160,89 @@ export function convertRuntimeToPlugin(
|
||||
const entryPath = join(dirname(entrypoint), entryBase);
|
||||
const entry = join(entryRoot, entryPath);
|
||||
|
||||
// We never want to link here, only copy, because the launcher
|
||||
// file often has the same name for every entrypoint, which means that
|
||||
// every build for every entrypoint overwrites the launcher of the previous
|
||||
// one, so linking would end with a broken reference.
|
||||
// Create the parent directory of the API Route that will be created
|
||||
// for the current entrypoint inside of `.output/server/pages/api`.
|
||||
await fs.ensureDir(dirname(entry));
|
||||
await fs.copy(handlerFile.fsPath, entry);
|
||||
|
||||
const newFilesEntrypoint: Array<string> = [];
|
||||
const newDirectoriesEntrypoint: Array<string> = [];
|
||||
// For compiled languages, the launcher file will be binary and therefore
|
||||
// won't try to import a user-provided request handler (instead, it will
|
||||
// contain it). But for interpreted languages, the launcher might try to
|
||||
// load a user-provided request handler from the source file instead of bundling
|
||||
// it, so we have to adjust the import statement inside the launcher to point
|
||||
// to the respective source file. Previously, Legacy Runtimes simply expected
|
||||
// the user-provided request-handler to be copied right next to the launcher,
|
||||
// but with the new File System API, files won't be moved around unnecessarily.
|
||||
if (handlerHasImport) {
|
||||
const { fsPath } = handlerFile;
|
||||
const encoding = 'utf-8';
|
||||
|
||||
const preBuildFiles = Object.values(sourceFilesPreBuild).map(file => {
|
||||
return file.fsPath;
|
||||
});
|
||||
// This is the true directory of the user-provided request handler in the
|
||||
// source files, so that's what we will use as an import path in the launcher.
|
||||
const locationPrefix = relative(entry, outputPath);
|
||||
|
||||
// Generate a list of directories and files that weren't present
|
||||
// before the entrypoint was processed by the Legacy Runtime, so
|
||||
// that we can perform a cleanup later. We need to divide into files
|
||||
// and directories because only cleaning up files might leave empty
|
||||
// directories, and listing directories separately also speeds up the
|
||||
// build because we can just delete them, which wipes all of their nested
|
||||
// paths, instead of iterating through all files that should be deleted.
|
||||
for (const file in sourceFilesAfterBuild) {
|
||||
if (!sourceFilesPreBuild[file]) {
|
||||
const path = sourceFilesAfterBuild[file].fsPath;
|
||||
const dirPath = dirname(path);
|
||||
let handlerContent = await fs.readFile(fsPath, encoding);
|
||||
|
||||
// If none of the files that were present before the entrypoint
|
||||
// was processed are contained within the directory we're looking
|
||||
// at right now, then we know it's a newly added directory
|
||||
// and it can therefore be removed later on.
|
||||
const isNewDir = !preBuildFiles.some(filePath => {
|
||||
return dirname(filePath).startsWith(dirPath);
|
||||
});
|
||||
const importPaths = [
|
||||
// This is the full entrypoint path, like `./api/test.py`. In our tests
|
||||
// Python didn't support importing from a parent directory without using different
|
||||
// code in the launcher that registers it as a location for modules and then changing
|
||||
// the importing syntax, but continuing to import it like before seems to work. If
|
||||
// other languages need this, we should consider excluding Python explicitly.
|
||||
// `./${entrypoint}`,
|
||||
|
||||
// Check out the list of tracked directories that were
|
||||
// newly added and see if one of them contains the path
|
||||
// we're looking at.
|
||||
const hasParentDir = newDirectoriesEntrypoint.some(dir => {
|
||||
return path.startsWith(dir);
|
||||
});
|
||||
// This is the entrypoint path without extension, like `api/test`
|
||||
entrypoint.slice(0, -ext.length),
|
||||
];
|
||||
|
||||
// If we have already tracked a directory that was newly
|
||||
// added that sits above the file or directory that we're
|
||||
// looking at, we don't need to add more entries to the list
|
||||
// because when the parent will get removed in the future,
|
||||
// all of its children (and therefore the path we're looking at)
|
||||
// will automatically get removed anyways.
|
||||
if (hasParentDir) {
|
||||
continue;
|
||||
}
|
||||
// Generate a list of regular expressions that we can use for
|
||||
// finding matches, but only allow matches if the import path is
|
||||
// wrapped inside single (') or double quotes (").
|
||||
const patterns = importPaths.map(path => {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return new RegExp(`('|")(${path.replace(/\./g, '\\.')})('|")`, 'g');
|
||||
});
|
||||
|
||||
if (isNewDir) {
|
||||
newDirectoriesEntrypoint.push(dirPath);
|
||||
} else {
|
||||
newFilesEntrypoint.push(path);
|
||||
let replacedMatch = null;
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const newContent = handlerContent.replace(
|
||||
pattern,
|
||||
(_, p1, p2, p3) => {
|
||||
return `${p1}${join(locationPrefix, p2)}${p3}`;
|
||||
}
|
||||
);
|
||||
|
||||
if (newContent !== handlerContent) {
|
||||
debug(
|
||||
`Replaced "${pattern}" inside "${entry}" to ensure correct import of user-provided request handler`
|
||||
);
|
||||
|
||||
handlerContent = newContent;
|
||||
replacedMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replacedMatch) {
|
||||
new Error(
|
||||
`No replacable matches for "${importPaths[0]}" or "${importPaths[1]}" found in "${fsPath}"`
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeFile(entry, handlerContent, encoding);
|
||||
} else {
|
||||
await fs.copy(handlerFile.fsPath, entry);
|
||||
}
|
||||
|
||||
// Legacy Runtimes based on interpreted languages will create a new launcher file
|
||||
// for every entrypoint, but they will create each one inside `workPath`, which means that
|
||||
// the launcher for one entrypoint will overwrite the launcher provided for the previous
|
||||
// entrypoint. That's why, above, we copy the file contents into the new destination (and
|
||||
// optionally transform them along the way), instead of linking. We then also want to remove
|
||||
// the copy origin right here, so that the `workPath` doesn't contain a useless launcher file
|
||||
// once the build has finished running.
|
||||
await fs.remove(handlerFile.fsPath);
|
||||
debug(`Removed temporary file "${handlerFile.fsPath}"`);
|
||||
|
||||
const nft = `${entry}.nft.json`;
|
||||
|
||||
const json = JSON.stringify({
|
||||
@@ -255,18 +267,8 @@ export function convertRuntimeToPlugin(
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
await fs.ensureDir(dirname(nft));
|
||||
await fs.writeFile(nft, json);
|
||||
|
||||
// Extend the list of directories and files that were created by the
|
||||
// Legacy Runtime with the list of directories and files that were
|
||||
// created for the entrypoint that was just processed above.
|
||||
newPathsRuntime = new Set([
|
||||
...newPathsRuntime,
|
||||
...newFilesEntrypoint,
|
||||
...newDirectoriesEntrypoint,
|
||||
]);
|
||||
|
||||
// Add an entry that will later on be added to the `functions-manifest.json`
|
||||
// file that is placed inside of the `.output` directory.
|
||||
pages[normalizePath(entryPath)] = {
|
||||
@@ -283,20 +285,6 @@ export function convertRuntimeToPlugin(
|
||||
};
|
||||
}
|
||||
|
||||
// A list of all the files that were created by the Legacy Runtime,
|
||||
// which we'd like to remove from the File System.
|
||||
const toRemove = Array.from(newPathsRuntime).map(path => {
|
||||
debug(`Removing ${path} as part of cleanup`);
|
||||
return fs.remove(path);
|
||||
});
|
||||
|
||||
// Once all the entrypoints have been processed, we'd like to
|
||||
// remove all the files from `workPath` that originally weren't present
|
||||
// before the Legacy Runtime began running, because the `workPath`
|
||||
// is nowadays the directory in which the user keeps their source code, since
|
||||
// we're no longer running separate parallel builds for every Legacy Runtime.
|
||||
await Promise.all(toRemove);
|
||||
|
||||
// Add any Serverless Functions that were exposed by the Legacy Runtime
|
||||
// to the `functions-manifest.json` file provided in `.output`.
|
||||
await updateFunctionsManifest({ workPath, pages });
|
||||
|
||||
@@ -3,7 +3,13 @@ import { valid as validSemver } from 'semver';
|
||||
import { parse as parsePath, extname } from 'path';
|
||||
import { Route, Source } from '@vercel/routing-utils';
|
||||
import frameworkList, { Framework } from '@vercel/frameworks';
|
||||
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
|
||||
import {
|
||||
PackageJson,
|
||||
Builder,
|
||||
Config,
|
||||
BuilderFunctions,
|
||||
ProjectSettings,
|
||||
} from './types';
|
||||
import { isOfficialRuntime } from './';
|
||||
const slugToFramework = new Map<string | null, Framework>(
|
||||
frameworkList.map(f => [f.slug, f])
|
||||
@@ -20,14 +26,7 @@ interface Options {
|
||||
tag?: 'canary' | 'latest' | string;
|
||||
functions?: BuilderFunctions;
|
||||
ignoreBuildScript?: boolean;
|
||||
projectSettings?: {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
installCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
createdAt?: number;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
cleanUrls?: boolean;
|
||||
trailingSlash?: boolean;
|
||||
featHandleMiss?: boolean;
|
||||
|
||||
237
packages/build-utils/src/detect-file-system-api.ts
Normal file
237
packages/build-utils/src/detect-file-system-api.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import semver from 'semver';
|
||||
import { isOfficialRuntime } from './';
|
||||
import type {
|
||||
Builder,
|
||||
BuilderFunctions,
|
||||
PackageJson,
|
||||
ProjectSettings,
|
||||
} from './types';
|
||||
|
||||
interface Metadata {
|
||||
plugins: string[];
|
||||
hasDotOutput: boolean;
|
||||
hasMiddleware: boolean;
|
||||
}
|
||||
|
||||
const enableFileSystemApiFrameworks = new Set(['solidstart']);
|
||||
|
||||
/**
|
||||
* If the Deployment can be built with the new File System API,
|
||||
* return the new Builder. Otherwise an "Exclusion Condition"
|
||||
* was hit so return `null` builder with a `reason` for exclusion.
|
||||
*/
|
||||
export async function detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings,
|
||||
builders,
|
||||
vercelConfig,
|
||||
pkg,
|
||||
tag,
|
||||
enableFlag = false,
|
||||
}: {
|
||||
files: { [relPath: string]: any };
|
||||
projectSettings: ProjectSettings;
|
||||
builders: Builder[];
|
||||
vercelConfig:
|
||||
| { builds?: Builder[]; functions?: BuilderFunctions }
|
||||
| null
|
||||
| undefined;
|
||||
pkg: PackageJson | null | undefined;
|
||||
tag: string | undefined;
|
||||
enableFlag: boolean | undefined;
|
||||
}): Promise<
|
||||
| { metadata: Metadata; fsApiBuilder: Builder; reason: null }
|
||||
| { metadata: Metadata; fsApiBuilder: null; reason: string }
|
||||
> {
|
||||
const framework = projectSettings.framework || '';
|
||||
const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
|
||||
const plugins = Object.keys(deps).filter(dep =>
|
||||
dep.startsWith('vercel-plugin-')
|
||||
);
|
||||
const hasDotOutput = Object.keys(files).some(file =>
|
||||
file.startsWith('.output/')
|
||||
);
|
||||
const hasMiddleware = Boolean(
|
||||
files['_middleware.js'] || files['_middleware.ts']
|
||||
);
|
||||
|
||||
const metadata: Metadata = {
|
||||
plugins,
|
||||
hasDotOutput,
|
||||
hasMiddleware,
|
||||
};
|
||||
|
||||
const isEnabled =
|
||||
enableFlag ||
|
||||
hasMiddleware ||
|
||||
hasDotOutput ||
|
||||
enableFileSystemApiFrameworks.has(framework);
|
||||
if (!isEnabled) {
|
||||
return { metadata, fsApiBuilder: null, reason: 'Flag not enabled.' };
|
||||
}
|
||||
|
||||
if (vercelConfig?.builds && vercelConfig.builds.length > 0) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `builds` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.values(vercelConfig?.functions || {}).some(fn => !!fn.runtime)) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `functions.runtime` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.HUGO_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `HUGO_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.ZOLA_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `ZOLA_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.GUTENBERG_VERSION) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `GUTENBERG_VERSION` environment variable. Please remove it.',
|
||||
};
|
||||
}
|
||||
|
||||
const invalidBuilder = builders.find(({ use }) => {
|
||||
const valid =
|
||||
isOfficialRuntime('go', use) ||
|
||||
isOfficialRuntime('python', use) ||
|
||||
isOfficialRuntime('ruby', use) ||
|
||||
isOfficialRuntime('node', use) ||
|
||||
isOfficialRuntime('next', use) ||
|
||||
isOfficialRuntime('static', use) ||
|
||||
isOfficialRuntime('static-build', use);
|
||||
return !valid;
|
||||
});
|
||||
|
||||
if (invalidBuilder) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected \`${invalidBuilder.use}\` in vercel.json. Please remove it in favor of CLI plugins.`,
|
||||
};
|
||||
}
|
||||
|
||||
for (const lang of ['go', 'python', 'ruby']) {
|
||||
for (const { use } of builders) {
|
||||
const plugin = 'vercel-plugin-' + lang;
|
||||
if (isOfficialRuntime(lang, use) && !deps[plugin]) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected \`${lang}\` Serverless Function usage without plugin \`${plugin}\`. Please run \`npm i ${plugin}\`.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
framework === 'nuxtjs' ||
|
||||
framework === 'sveltekit' ||
|
||||
framework === 'redwoodjs'
|
||||
) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected framework \`${framework}\` that only supports legacy File System API. Please contact the framework author.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (framework === 'nextjs' && !hasDotOutput) {
|
||||
// Use the old pipeline if a custom output directory was specified for Next.js
|
||||
// because `vercel build` cannot ensure that the directory will be in the same
|
||||
// location as `.output`, which can break imports (not just nft.json files).
|
||||
if (projectSettings?.outputDirectory) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected Next.js with Output Directory \`${projectSettings.outputDirectory}\` override. Please change it back to the default.`,
|
||||
};
|
||||
}
|
||||
const nextVersion = deps['next'];
|
||||
if (!nextVersion) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected Next.js in Project Settings but missing \`next\` package.json dependencies. Please run \`npm i next\`.`,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Read version from lockfile instead of package.json
|
||||
if (nextVersion !== 'latest' && nextVersion !== 'canary') {
|
||||
const fixedVersion = semver.valid(semver.coerce(nextVersion) || '');
|
||||
|
||||
if (!fixedVersion || !semver.gte(fixedVersion, '12.0.0')) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected legacy Next.js version "${nextVersion}" in package.json. Please run \`npm i next@latest\` to upgrade.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDotOutput) {
|
||||
// TODO: Read version from lockfile instead of package.json
|
||||
const vercelCliVersion = deps['vercel'];
|
||||
if (
|
||||
vercelCliVersion &&
|
||||
vercelCliVersion !== 'latest' &&
|
||||
vercelCliVersion !== 'canary'
|
||||
) {
|
||||
const fixedVersion = semver.valid(semver.coerce(vercelCliVersion) || '');
|
||||
// TODO: we might want to use '24.0.0' once its released
|
||||
if (!fixedVersion || !semver.gte(fixedVersion, '23.1.3-canary.68')) {
|
||||
return {
|
||||
metadata,
|
||||
fsApiBuilder: null,
|
||||
reason: `Detected legacy Vercel CLI version "${vercelCliVersion}" in package.json. Please run \`npm i vercel@latest\` to upgrade.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const frontendBuilder = builders.find(
|
||||
({ use }) =>
|
||||
isOfficialRuntime('next', use) ||
|
||||
isOfficialRuntime('static', use) ||
|
||||
isOfficialRuntime('static-build', use)
|
||||
);
|
||||
const config = frontendBuilder?.config || {};
|
||||
const withTag = tag ? `@${tag}` : '';
|
||||
|
||||
const fsApiBuilder = {
|
||||
use: `@vercelruntimes/file-system-api${withTag}`,
|
||||
src: '**',
|
||||
config: {
|
||||
...config,
|
||||
fileSystemAPI: true,
|
||||
framework: config.framework || framework || null,
|
||||
projectSettings,
|
||||
hasMiddleware,
|
||||
hasDotOutput,
|
||||
},
|
||||
};
|
||||
return { metadata, fsApiBuilder, reason: null };
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export {
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from './detect-builders';
|
||||
export { detectFileSystemAPI } from './detect-file-system-api';
|
||||
export { detectFramework } from './detect-framework';
|
||||
export { DetectorFilesystem } from './detectors/filesystem';
|
||||
export { readConfigFile } from './fs/read-config-file';
|
||||
|
||||
@@ -29,7 +29,9 @@ export interface Config {
|
||||
| number
|
||||
| { [key: string]: string }
|
||||
| BuilderFunctions
|
||||
| undefined;
|
||||
| ProjectSettings
|
||||
| undefined
|
||||
| null;
|
||||
maxLambdaSize?: string;
|
||||
includeFiles?: string | string[];
|
||||
excludeFiles?: string | string[];
|
||||
@@ -41,11 +43,12 @@ export interface Config {
|
||||
zeroConfig?: boolean;
|
||||
import?: { [key: string]: string };
|
||||
functions?: BuilderFunctions;
|
||||
projectSettings?: ProjectSettings;
|
||||
outputDirectory?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
devCommand?: string;
|
||||
framework?: string;
|
||||
framework?: string | null;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
@@ -351,3 +354,17 @@ export interface BuilderFunctions {
|
||||
excludeFiles?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectSettings {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
installCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
createdAt?: number;
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
sourceFilesOutsideRootDirectory?: boolean;
|
||||
directoryListing?: boolean;
|
||||
gitForkProtection?: boolean;
|
||||
}
|
||||
|
||||
445
packages/build-utils/test/unit.detect-file-system-api.test.ts
vendored
Normal file
445
packages/build-utils/test/unit.detect-file-system-api.test.ts
vendored
Normal file
@@ -0,0 +1,445 @@
|
||||
import { detectFileSystemAPI } from '../src/detect-file-system-api';
|
||||
|
||||
describe('Test `detectFileSystemAPI`', () => {
|
||||
it('should error when builds in vercel.json', async () => {
|
||||
const vercelConfig = {
|
||||
builds: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
};
|
||||
const files = {
|
||||
'vercel.json': JSON.stringify(vercelConfig),
|
||||
'api/foo.js': 'console.log("foo")',
|
||||
};
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: vercelConfig.builds,
|
||||
vercelConfig,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `builds` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when functions.runtimes in vercel.json', async () => {
|
||||
const vercelConfig = {
|
||||
functions: {
|
||||
'api/**/*.rs': {
|
||||
runtime: 'vercel-rust@latest',
|
||||
},
|
||||
},
|
||||
};
|
||||
const files = {
|
||||
'vercel.json': JSON.stringify(vercelConfig),
|
||||
'api/foo.rs': 'println!("foo")',
|
||||
};
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `functions.runtime` in vercel.json. Please remove it in favor of CLI plugins.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when HUGO_VERSION env var used', async () => {
|
||||
process.env.HUGO_VERSION = 'v0.58.2';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `HUGO_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.HUGO_VERSION;
|
||||
});
|
||||
|
||||
it('should error when ZOLA_VERSION env var used', async () => {
|
||||
process.env.ZOLA_VERSION = 'v0.0.1';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason: 'Detected `ZOLA_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.ZOLA_VERSION;
|
||||
});
|
||||
|
||||
it('should error when GUTENBERG_VERSION env var used', async () => {
|
||||
process.env.GUTENBERG_VERSION = 'v0.0.1';
|
||||
const files = { 'foo.html': '<h1>Foo</h1>' };
|
||||
const result = await detectFileSystemAPI({
|
||||
files,
|
||||
projectSettings: {},
|
||||
builders: [],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `GUTENBERG_VERSION` environment variable. Please remove it.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
delete process.env.GUTENBERG_VERSION;
|
||||
});
|
||||
|
||||
it('should error when Go detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.go': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/go', src: 'api/**/*.go' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `go` Serverless Function usage without plugin `vercel-plugin-go`. Please run `npm i vercel-plugin-go`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when Python detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.py': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/python', src: 'api/**/*.py' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `python` Serverless Function usage without plugin `vercel-plugin-python`. Please run `npm i vercel-plugin-python`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when Ruby detected without corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.rb': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/ruby', src: 'api/**/*.rb' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected `ruby` Serverless Function usage without plugin `vercel-plugin-ruby`. Please run `npm i vercel-plugin-ruby`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Go detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.go': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/go', src: 'api/**/*.go' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-go': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-go'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Python detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.py': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/python', src: 'api/**/*.py' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-python': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-python'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when Ruby detected with corresponding plugin', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.rb': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/ruby', src: 'api/**/*.rb' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'vercel-plugin-ruby': '^1.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: {
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: false,
|
||||
plugins: ['vercel-plugin-ruby'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nuxtjs', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nuxtjs' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `nuxtjs` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is sveltekit', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'sveltekit' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `sveltekit` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is redwoodjs', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'api/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'redwoodjs' },
|
||||
builders: [{ use: '@vercel/node', src: 'api/**/*.js' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected framework `redwoodjs` that only supports legacy File System API. Please contact the framework author.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs and has output dir', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs', outputDirectory: 'dist' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { next: '^12.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected Next.js with Output Directory `dist` override. Please change it back to the default.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs but missing from dependencies', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { 'not-next': '^12.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected Next.js in Project Settings but missing `next` package.json dependencies. Please run `npm i next`.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when framework is nextjs but dependency is older version', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { next: '^9.0.0' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected legacy Next.js version "^9.0.0" in package.json. Please run `npm i next@latest` to upgrade.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should error when vercel cli is older version', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { 'pages/foo.js': 'console.log("foo")' },
|
||||
projectSettings: { framework: 'nextjs' },
|
||||
builders: [{ use: '@vercel/next', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: { dependencies: { next: '^12.1.0', vercel: '^23.1.1' } },
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: null,
|
||||
reason:
|
||||
'Detected legacy Vercel CLI version "^23.1.1" in package.json. Please run `npm i vercel@latest` to upgrade.',
|
||||
metadata: { hasDotOutput: false, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when middleware detected', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { '_middleware.js': 'print("foo")' },
|
||||
projectSettings: {},
|
||||
builders: [{ use: '@vercel/static-build', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: null,
|
||||
hasDotOutput: false,
|
||||
hasMiddleware: true,
|
||||
projectSettings: {},
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: { hasDotOutput: false, hasMiddleware: true, plugins: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed when .output detected', async () => {
|
||||
const result = await detectFileSystemAPI({
|
||||
files: { '.output/routes-manifest.json': '{}' },
|
||||
projectSettings: { framework: 'remix' },
|
||||
builders: [{ use: '@vercel/static-build', src: 'package.json' }],
|
||||
vercelConfig: null,
|
||||
pkg: null,
|
||||
tag: '',
|
||||
enableFlag: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
fsApiBuilder: {
|
||||
use: '@vercelruntimes/file-system-api',
|
||||
src: '**',
|
||||
config: {
|
||||
fileSystemAPI: true,
|
||||
framework: 'remix',
|
||||
hasDotOutput: true,
|
||||
hasMiddleware: false,
|
||||
projectSettings: { framework: 'remix' },
|
||||
},
|
||||
},
|
||||
reason: null,
|
||||
metadata: { hasDotOutput: true, hasMiddleware: false, plugins: [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ Finally, [connect your Git repository to Vercel](https://vercel.com/docs/git) an
|
||||
|
||||
## Documentation
|
||||
|
||||
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs).
|
||||
For details on how to use Vercel CLI, check out our [documentation](https://vercel.com/docs/cli).
|
||||
|
||||
## Local Development
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "23.1.3-canary.62",
|
||||
"version": "23.1.3-canary.69",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -43,14 +43,14 @@
|
||||
"node": ">= 12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/go": "1.2.4-canary.4",
|
||||
"@vercel/node": "1.12.2-canary.7",
|
||||
"@vercel/python": "2.1.2-canary.1",
|
||||
"@vercel/python": "2.1.2-canary.2",
|
||||
"@vercel/ruby": "1.2.10-canary.0",
|
||||
"update-notifier": "4.1.0",
|
||||
"vercel-plugin-middleware": "0.0.0-canary.15",
|
||||
"vercel-plugin-node": "1.12.2-canary.31"
|
||||
"vercel-plugin-middleware": "0.0.0-canary.21",
|
||||
"vercel-plugin-node": "1.12.2-canary.36"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "11.1.2",
|
||||
@@ -90,6 +90,7 @@
|
||||
"@types/update-notifier": "5.1.0",
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@vercel/client": "10.2.3-canary.47",
|
||||
"@vercel/frameworks": "0.5.1-canary.17",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.0",
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
scanParentDirs,
|
||||
spawnAsync,
|
||||
glob as buildUtilsGlob,
|
||||
detectFileSystemAPI,
|
||||
detectBuilders,
|
||||
PackageJson,
|
||||
} from '@vercel/build-utils';
|
||||
import { nodeFileTrace } from '@vercel/nft';
|
||||
import Sema from 'async-sema';
|
||||
@@ -19,6 +22,7 @@ import pluralize from 'pluralize';
|
||||
import Client from '../util/client';
|
||||
import { VercelConfig } from '../util/dev/types';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
import { CantParseJSONFile } from '../util/errors-ts';
|
||||
import getArgs from '../util/get-args';
|
||||
import handleError from '../util/handle-error';
|
||||
import confirm from '../util/input/confirm';
|
||||
@@ -32,6 +36,7 @@ import { loadCliPlugins } from '../util/plugins';
|
||||
import { findFramework } from '../util/projects/find-framework';
|
||||
import { VERCEL_DIR } from '../util/projects/link';
|
||||
import { readProjectSettings } from '../util/projects/project-settings';
|
||||
import readJSONFile from '../util/read-json-file';
|
||||
import pull from './pull';
|
||||
|
||||
const sema = new Sema(16, {
|
||||
@@ -146,6 +151,36 @@ export default async function main(client: Client) {
|
||||
|
||||
process.chdir(cwd);
|
||||
|
||||
const pkg = await readJSONFile<PackageJson>('./package.json');
|
||||
if (pkg instanceof CantParseJSONFile) {
|
||||
throw pkg;
|
||||
}
|
||||
const vercelConfig = await readJSONFile<VercelConfig>('./vercel.json');
|
||||
if (vercelConfig instanceof CantParseJSONFile) {
|
||||
throw vercelConfig;
|
||||
}
|
||||
|
||||
if (!process.env.NOW_BUILDER) {
|
||||
// This validation is only necessary when
|
||||
// a user runs `vercel build` locally.
|
||||
const globFiles = await buildUtilsGlob('**', { cwd });
|
||||
const zeroConfig = await detectBuilders(Object.keys(globFiles), pkg);
|
||||
const { reason } = await detectFileSystemAPI({
|
||||
files: globFiles,
|
||||
projectSettings: project.settings,
|
||||
builders: zeroConfig.builders || [],
|
||||
pkg,
|
||||
vercelConfig,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
|
||||
if (reason) {
|
||||
client.output.error(`${cmd(`${getPkgName()} build`)} failed: ${reason}`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const framework = findFramework(project.settings.framework);
|
||||
// If this is undefined, we bail. If it is null, then findFramework should return "Other",
|
||||
// so this should really never happen, but just in case....
|
||||
|
||||
@@ -166,8 +166,8 @@ export default async (client: Client) => {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
const { isFile, path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'] || isFile;
|
||||
const { path } = pathValidation;
|
||||
const autoConfirm = argv['--confirm'];
|
||||
|
||||
// deprecate --name
|
||||
if (argv['--name']) {
|
||||
@@ -192,7 +192,7 @@ export default async (client: Client) => {
|
||||
|
||||
let newProjectName = null;
|
||||
let rootDirectory = project ? project.rootDirectory : null;
|
||||
let sourceFilesOutsideRootDirectory = true;
|
||||
let sourceFilesOutsideRootDirectory: boolean | undefined = true;
|
||||
|
||||
if (status === 'not_linked') {
|
||||
const shouldStartSetup =
|
||||
@@ -229,8 +229,7 @@ export default async (client: Client) => {
|
||||
// user input.
|
||||
const detectedProjectName = getProjectName({
|
||||
argv,
|
||||
nowConfig: localConfig || {},
|
||||
isFile,
|
||||
nowConfig: localConfig,
|
||||
paths,
|
||||
});
|
||||
|
||||
@@ -447,9 +446,9 @@ export default async (client: Client) => {
|
||||
forceNew: argv['--force'],
|
||||
withCache: argv['--with-cache'],
|
||||
prebuilt: argv['--prebuilt'],
|
||||
rootDirectory,
|
||||
quiet,
|
||||
wantsPublic: argv['--public'] || localConfig.public,
|
||||
isFile,
|
||||
type: null,
|
||||
nowConfig: localConfig,
|
||||
regions,
|
||||
@@ -471,7 +470,7 @@ export default async (client: Client) => {
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!project && !isFile,
|
||||
!project,
|
||||
path
|
||||
);
|
||||
|
||||
@@ -654,8 +653,7 @@ export default async (client: Client) => {
|
||||
client,
|
||||
deployment,
|
||||
deployStamp,
|
||||
!argv['--no-clipboard'],
|
||||
isFile
|
||||
!argv['--no-clipboard']
|
||||
);
|
||||
};
|
||||
|
||||
@@ -790,8 +788,7 @@ const printDeploymentStatus = async (
|
||||
};
|
||||
},
|
||||
deployStamp: () => string,
|
||||
isClipboardEnabled: boolean,
|
||||
isFile: boolean
|
||||
isClipboardEnabled: boolean
|
||||
) => {
|
||||
indications = indications || [];
|
||||
const isProdDeployment = target === 'production';
|
||||
@@ -813,7 +810,7 @@ const printDeploymentStatus = async (
|
||||
// print preview/production url
|
||||
let previewUrl: string;
|
||||
let isWildcard: boolean;
|
||||
if (!isFile && Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
if (Array.isArray(aliasList) && aliasList.length > 0) {
|
||||
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
|
||||
if (previewUrlInfo) {
|
||||
isWildcard = previewUrlInfo.isWildcard;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type ProjectSettings = import('@vercel/build-utils').ProjectSettings;
|
||||
|
||||
export type Primitive =
|
||||
| bigint
|
||||
| boolean
|
||||
@@ -239,16 +241,6 @@ export interface ProjectEnvVariable {
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
export interface ProjectSettings {
|
||||
framework?: string | null;
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
directoryListing?: boolean;
|
||||
}
|
||||
|
||||
export interface Project extends ProjectSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -260,8 +252,6 @@ export interface Project extends ProjectSettings {
|
||||
framework?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
autoExposeSystemEnvs?: boolean;
|
||||
sourceFilesOutsideRootDirectory: boolean;
|
||||
}
|
||||
|
||||
export interface Org {
|
||||
|
||||
@@ -6,14 +6,14 @@ import getLocalConfigPath from './local-path';
|
||||
|
||||
export default async function readConfig(dir: string) {
|
||||
const pkgFilePath = getLocalConfigPath(join(process.cwd(), dir));
|
||||
const result = await readJSONFile(pkgFilePath);
|
||||
const result = await readJSONFile<VercelConfig>(pkgFilePath);
|
||||
|
||||
if (result instanceof CantParseJSONFile) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return result as VercelConfig;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -52,6 +52,7 @@ export default async function processDeployment({
|
||||
isSettingUpProject: boolean;
|
||||
skipAutoDetectionConfirmation?: boolean;
|
||||
cwd?: string;
|
||||
rootDirectory?: string;
|
||||
}) {
|
||||
let {
|
||||
now,
|
||||
@@ -64,6 +65,7 @@ export default async function processDeployment({
|
||||
nowConfig,
|
||||
quiet,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
} = args;
|
||||
|
||||
const { debug } = output;
|
||||
@@ -86,6 +88,7 @@ export default async function processDeployment({
|
||||
force,
|
||||
withCache,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
skipAutoDetectionConfirmation,
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
detectApiExtensions,
|
||||
spawnCommand,
|
||||
isOfficialRuntime,
|
||||
detectFileSystemAPI,
|
||||
} from '@vercel/build-utils';
|
||||
import frameworkList from '@vercel/frameworks';
|
||||
|
||||
@@ -599,6 +600,32 @@ export default class DevServer {
|
||||
);
|
||||
}
|
||||
|
||||
const { reason, metadata } = await detectFileSystemAPI({
|
||||
files,
|
||||
builders: builders || [],
|
||||
projectSettings: projectSettings || this.projectSettings || {},
|
||||
vercelConfig,
|
||||
pkg,
|
||||
tag: '',
|
||||
enableFlag: true,
|
||||
});
|
||||
|
||||
if (reason) {
|
||||
if (metadata.hasMiddleware) {
|
||||
this.output.error(
|
||||
`Detected middleware usage which requires the latest API. ${reason}`
|
||||
);
|
||||
await this.exit();
|
||||
} else if (metadata.plugins.length > 0) {
|
||||
this.output.error(
|
||||
`Detected CLI plugins which requires the latest API. ${reason}`
|
||||
);
|
||||
await this.exit();
|
||||
} else {
|
||||
this.output.warn(`Unable to use latest API. ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (builders) {
|
||||
if (this.devCommand) {
|
||||
builders = builders.filter(filterFrontendBuilds);
|
||||
|
||||
@@ -39,12 +39,12 @@ export default async function getConfig(
|
||||
output.debug(
|
||||
`Found config in provided --local-config path ${localFilePath}`
|
||||
);
|
||||
const localConfig = await readJSONFile(localFilePath);
|
||||
const localConfig = await readJSONFile<VercelConfig>(localFilePath);
|
||||
if (localConfig instanceof CantParseJSONFile) {
|
||||
return localConfig;
|
||||
}
|
||||
if (localConfig !== null) {
|
||||
config = localConfig as VercelConfig;
|
||||
config = localConfig;
|
||||
config[fileNameSymbol] = configFile;
|
||||
return config;
|
||||
}
|
||||
@@ -54,8 +54,8 @@ export default async function getConfig(
|
||||
const vercelFilePath = path.resolve(localPath, 'vercel.json');
|
||||
const nowFilePath = path.resolve(localPath, 'now.json');
|
||||
const [vercelConfig, nowConfig] = await Promise.all([
|
||||
readJSONFile(vercelFilePath),
|
||||
readJSONFile(nowFilePath),
|
||||
readJSONFile<VercelConfig>(vercelFilePath),
|
||||
readJSONFile<VercelConfig>(nowFilePath),
|
||||
]);
|
||||
if (vercelConfig instanceof CantParseJSONFile) {
|
||||
return vercelConfig;
|
||||
@@ -68,13 +68,13 @@ export default async function getConfig(
|
||||
}
|
||||
if (vercelConfig !== null) {
|
||||
output.debug(`Found config in file "${vercelFilePath}"`);
|
||||
config = vercelConfig as VercelConfig;
|
||||
config = vercelConfig;
|
||||
config[fileNameSymbol] = 'vercel.json';
|
||||
return config;
|
||||
}
|
||||
if (nowConfig !== null) {
|
||||
output.debug(`Found config in file "${nowFilePath}"`);
|
||||
config = nowConfig as VercelConfig;
|
||||
config = nowConfig;
|
||||
config[fileNameSymbol] = 'now.json';
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import { VercelConfig } from '@vercel/client';
|
||||
export interface GetProjectNameOptions {
|
||||
argv: { '--name'?: string };
|
||||
nowConfig?: VercelConfig;
|
||||
isFile?: boolean;
|
||||
paths?: string[];
|
||||
}
|
||||
|
||||
export default function getProjectName({
|
||||
argv,
|
||||
nowConfig = {},
|
||||
isFile = false,
|
||||
paths = [],
|
||||
}: GetProjectNameOptions) {
|
||||
const nameCli = argv['--name'];
|
||||
@@ -24,10 +22,6 @@ export default function getProjectName({
|
||||
return nowConfig.name;
|
||||
}
|
||||
|
||||
if (isFile || paths.length > 1) {
|
||||
return 'files';
|
||||
}
|
||||
|
||||
// Otherwise, use the name of the directory
|
||||
return basename(paths[0] || '');
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ export interface NowOptions {
|
||||
export interface CreateOptions {
|
||||
// Legacy
|
||||
nowConfig?: VercelConfig;
|
||||
isFile?: boolean;
|
||||
|
||||
// Latest
|
||||
name: string;
|
||||
project?: string;
|
||||
wantsPublic: boolean;
|
||||
prebuilt?: boolean;
|
||||
rootDirectory?: string;
|
||||
meta: Dictionary<string>;
|
||||
regions?: string[];
|
||||
quiet?: boolean;
|
||||
@@ -113,6 +113,7 @@ export default class Now extends EventEmitter {
|
||||
name,
|
||||
project,
|
||||
prebuilt = false,
|
||||
rootDirectory,
|
||||
wantsPublic,
|
||||
meta,
|
||||
regions,
|
||||
@@ -168,6 +169,7 @@ export default class Now extends EventEmitter {
|
||||
skipAutoDetectionConfirmation,
|
||||
cwd,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
});
|
||||
|
||||
if (deployment && deployment.warnings) {
|
||||
|
||||
@@ -11,7 +11,7 @@ export default async function inputProject(
|
||||
client: Client,
|
||||
org: Org,
|
||||
detectedProjectName: string,
|
||||
autoConfirm: boolean
|
||||
autoConfirm = false
|
||||
): Promise<Project | string> {
|
||||
const { output } = client;
|
||||
const slugifiedName = slugify(detectedProjectName);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { validateRootDirectory } from '../validate-paths';
|
||||
export async function inputRootDirectory(
|
||||
cwd: string,
|
||||
output: Output,
|
||||
autoConfirm: boolean
|
||||
autoConfirm = false
|
||||
) {
|
||||
if (autoConfirm) {
|
||||
return null;
|
||||
|
||||
@@ -158,7 +158,6 @@ export default async function setupAndLink(
|
||||
withCache: undefined,
|
||||
quiet,
|
||||
wantsPublic: localConfig?.public || false,
|
||||
isFile,
|
||||
nowConfig: localConfig,
|
||||
regions: undefined,
|
||||
meta: {},
|
||||
@@ -179,7 +178,7 @@ export default async function setupAndLink(
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!isFile,
|
||||
true,
|
||||
path
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs-extra';
|
||||
import { CantParseJSONFile } from './errors-ts';
|
||||
|
||||
export default async function readJSONFile(
|
||||
export default async function readJSONFile<T>(
|
||||
file: string
|
||||
): Promise<Object | null | CantParseJSONFile> {
|
||||
): Promise<T | null | CantParseJSONFile> {
|
||||
const content = await readFileSafe(file);
|
||||
if (content === null) {
|
||||
return content;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Output } from './output';
|
||||
import chalk from 'chalk';
|
||||
import { homedir } from 'os';
|
||||
import confirm from './input/confirm';
|
||||
import { prependEmoji, emoji } from './emoji';
|
||||
import toHumanPath from './humanize-path';
|
||||
|
||||
const stat = promisify(lstatRaw);
|
||||
@@ -54,10 +53,7 @@ export async function validateRootDirectory(
|
||||
export default async function validatePaths(
|
||||
output: Output,
|
||||
paths: string[]
|
||||
): Promise<
|
||||
| { valid: true; path: string; isFile: boolean }
|
||||
| { valid: false; exitCode: number }
|
||||
> {
|
||||
): Promise<{ valid: true; path: string } | { valid: false; exitCode: number }> {
|
||||
// can't deploy more than 1 path
|
||||
if (paths.length > 1) {
|
||||
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 };
|
||||
}
|
||||
|
||||
const isFile = pathStat && !pathStat.isDirectory();
|
||||
if (isFile) {
|
||||
output.print(
|
||||
`${prependEmoji(
|
||||
'Deploying files with Vercel is deprecated (https://vercel.link/faq-deploy-file)',
|
||||
emoji('warning')
|
||||
)}\n`
|
||||
);
|
||||
if (!pathStat.isDirectory()) {
|
||||
output.prettyError({
|
||||
message: 'Support for single file deployments has been removed.',
|
||||
link: 'https://vercel.link/no-single-file-deployments',
|
||||
});
|
||||
return { valid: false, exitCode: 1 };
|
||||
}
|
||||
|
||||
// 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));
|
||||
});
|
||||
|
||||
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 => {
|
||||
const directory = fixture('build-env');
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('getProjectName', () => {
|
||||
expect(project).toEqual('abc');
|
||||
});
|
||||
|
||||
it('should work with now.json', () => {
|
||||
it('should work with `vercel.json` config', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
nowConfig: { name: 'abc' },
|
||||
@@ -18,24 +18,6 @@ describe('getProjectName', () => {
|
||||
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', () => {
|
||||
const project = getProjectName({
|
||||
argv: {},
|
||||
|
||||
2
packages/client/.gitignore
vendored
2
packages/client/.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules
|
||||
!tests/fixtures/nowignore/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/node_modules
|
||||
!tests/fixtures/vercelignore-allow-nodemodules/sub/node_modules
|
||||
!tests/fixtures/file-system-api/.output
|
||||
!tests/fixtures/file-system-api-root-directory/**/.output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "10.2.3-canary.40",
|
||||
"version": "10.2.3-canary.47",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -40,7 +40,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { lstatSync } from 'fs-extra';
|
||||
|
||||
import { relative, isAbsolute } from 'path';
|
||||
import hashes, { mapToObject } from './utils/hashes';
|
||||
import { hashes, mapToObject, resolveNftJsonFiles } from './utils/hashes';
|
||||
import { upload } from './upload';
|
||||
import { buildFileTree, createDebug, parseVercelConfig } from './utils';
|
||||
import { DeploymentError } from './errors';
|
||||
import {
|
||||
NowConfig,
|
||||
VercelConfig,
|
||||
VercelClientOptions,
|
||||
DeploymentOptions,
|
||||
DeploymentEventType,
|
||||
@@ -16,7 +16,7 @@ export default function buildCreateDeployment() {
|
||||
return async function* createDeployment(
|
||||
clientOptions: VercelClientOptions,
|
||||
deploymentOptions: DeploymentOptions = {},
|
||||
nowConfig: NowConfig = {}
|
||||
nowConfig: VercelConfig = {}
|
||||
): AsyncIterableIterator<{ type: DeploymentEventType; payload: any }> {
|
||||
const { path } = clientOptions;
|
||||
|
||||
@@ -74,12 +74,7 @@ export default function buildCreateDeployment() {
|
||||
debug(`Provided 'path' is a single file`);
|
||||
}
|
||||
|
||||
let { fileList } = await buildFileTree(
|
||||
path,
|
||||
clientOptions.isDirectory,
|
||||
debug,
|
||||
clientOptions.prebuilt
|
||||
);
|
||||
let { fileList } = await buildFileTree(path, clientOptions, debug);
|
||||
|
||||
let configPath: string | undefined;
|
||||
if (!nowConfig) {
|
||||
@@ -114,7 +109,11 @@ export default function buildCreateDeployment() {
|
||||
};
|
||||
}
|
||||
|
||||
const files = await hashes(fileList);
|
||||
const hashedFileMap = await hashes(fileList);
|
||||
const nftFileList = clientOptions.prebuilt
|
||||
? await resolveNftJsonFiles(hashedFileMap)
|
||||
: [];
|
||||
const files = await hashes(nftFileList, hashedFileMap);
|
||||
|
||||
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
|
||||
yield { type: 'hashes-calculated', payload: mapToObject(files) };
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Builder, BuilderFunctions } from '@vercel/build-utils';
|
||||
import {
|
||||
Builder,
|
||||
BuilderFunctions,
|
||||
ProjectSettings,
|
||||
} from '@vercel/build-utils';
|
||||
import { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
|
||||
|
||||
export { DeploymentEventType } from './utils';
|
||||
@@ -15,6 +19,7 @@ export interface VercelClientOptions {
|
||||
apiUrl?: string;
|
||||
force?: boolean;
|
||||
prebuilt?: boolean;
|
||||
rootDirectory?: string;
|
||||
withCache?: boolean;
|
||||
userAgent?: string;
|
||||
defaultName?: string;
|
||||
@@ -123,12 +128,7 @@ export interface VercelConfig {
|
||||
scope?: string;
|
||||
alias?: string | string[];
|
||||
regions?: string[];
|
||||
projectSettings?: {
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
framework?: string | null;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +154,5 @@ export interface DeploymentOptions {
|
||||
name?: string;
|
||||
public?: boolean;
|
||||
meta?: Dictionary<string>;
|
||||
projectSettings?: {
|
||||
devCommand?: string | null;
|
||||
buildCommand?: string | null;
|
||||
outputDirectory?: string | null;
|
||||
};
|
||||
projectSettings?: ProjectSettings;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import { Sema } from 'async-sema';
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
export interface DeploymentFile {
|
||||
names: string[];
|
||||
@@ -15,9 +16,7 @@ export interface DeploymentFile {
|
||||
* @return {String} hex digest
|
||||
*/
|
||||
function hash(buf: Buffer): string {
|
||||
return createHash('sha1')
|
||||
.update(buf)
|
||||
.digest('hex');
|
||||
return createHash('sha1').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,34 +38,68 @@ export const mapToObject = (
|
||||
/**
|
||||
* Computes hashes for the contents of each file given.
|
||||
*
|
||||
* @param {Array} of {String} full paths
|
||||
* @return {Map}
|
||||
* @param files - absolute file paths
|
||||
* @param map - optional map of files to append
|
||||
* @return Map of hash digest to file object
|
||||
*/
|
||||
async function hashes(files: string[]): Promise<Map<string, DeploymentFile>> {
|
||||
const map = new Map<string, DeploymentFile>();
|
||||
export async function hashes(
|
||||
files: string[],
|
||||
map = new Map<string, DeploymentFile>()
|
||||
): Promise<Map<string, DeploymentFile>> {
|
||||
const semaphore = new Sema(100);
|
||||
|
||||
await Promise.all(
|
||||
files.map(
|
||||
async (name: string): Promise<void> => {
|
||||
await semaphore.acquire();
|
||||
const data = await fs.readFile(name);
|
||||
const { mode } = await fs.stat(name);
|
||||
files.map(async (name: string): Promise<void> => {
|
||||
await semaphore.acquire();
|
||||
const data = await fs.readFile(name);
|
||||
const { mode } = await fs.stat(name);
|
||||
|
||||
const h = hash(data);
|
||||
const entry = map.get(h);
|
||||
const h = hash(data);
|
||||
const entry = map.get(h);
|
||||
|
||||
if (entry) {
|
||||
entry.names.push(name);
|
||||
} else {
|
||||
map.set(h, { names: [name], data, mode });
|
||||
}
|
||||
|
||||
semaphore.release();
|
||||
if (entry) {
|
||||
const names = new Set(entry.names);
|
||||
names.add(name);
|
||||
entry.names = [...names];
|
||||
} else {
|
||||
map.set(h, { names: [name], data, mode });
|
||||
}
|
||||
)
|
||||
|
||||
semaphore.release();
|
||||
})
|
||||
);
|
||||
return map;
|
||||
}
|
||||
|
||||
export default hashes;
|
||||
export async function resolveNftJsonFiles(
|
||||
hashedFiles: Map<string, DeploymentFile>
|
||||
): Promise<string[]> {
|
||||
const semaphore = new Sema(100);
|
||||
const existingFiles = Array.from(hashedFiles.values());
|
||||
const resolvedFiles = new Set<string>();
|
||||
|
||||
await Promise.all(
|
||||
existingFiles.map(async file => {
|
||||
await semaphore.acquire();
|
||||
const fsPath = file.names[0];
|
||||
if (fsPath.endsWith('.nft.json')) {
|
||||
const json = file.data.toString('utf8');
|
||||
const { version, files } = JSON.parse(json) as {
|
||||
version: number;
|
||||
files: string[] | { input: string; output: string }[];
|
||||
};
|
||||
if (version === 1 || version === 2) {
|
||||
for (let f of files) {
|
||||
const relPath = typeof f === 'string' ? f : f.input;
|
||||
resolvedFiles.add(join(dirname(fsPath), relPath));
|
||||
}
|
||||
} else {
|
||||
console.error(`Invalid nft.json version: ${version}`);
|
||||
}
|
||||
}
|
||||
semaphore.release();
|
||||
})
|
||||
);
|
||||
|
||||
return Array.from(resolvedFiles);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeploymentFile } from './hashes';
|
||||
import { FetchOptions } from '@zeit/fetch';
|
||||
import { nodeFetch, zeitFetch } from './fetch';
|
||||
import { join, sep, relative } from 'path';
|
||||
import { join, sep, relative, posix } from 'path';
|
||||
import { URL } from 'url';
|
||||
import ignore from 'ignore';
|
||||
type Ignore = ReturnType<typeof ignore>;
|
||||
@@ -81,13 +81,16 @@ const maybeRead = async function <T>(path: string, default_: T) {
|
||||
|
||||
export async function buildFileTree(
|
||||
path: string | string[],
|
||||
isDirectory: boolean,
|
||||
debug: Debug,
|
||||
prebuilt?: boolean
|
||||
{
|
||||
isDirectory,
|
||||
prebuilt,
|
||||
rootDirectory,
|
||||
}: Pick<VercelClientOptions, 'isDirectory' | 'prebuilt' | 'rootDirectory'>,
|
||||
debug: Debug
|
||||
): Promise<{ fileList: string[]; ignoreList: string[] }> {
|
||||
const ignoreList: string[] = [];
|
||||
let fileList: string[];
|
||||
let { ig, ignores } = await getVercelIgnore(path, prebuilt);
|
||||
let { ig, ignores } = await getVercelIgnore(path, prebuilt, rootDirectory);
|
||||
|
||||
debug(`Found ${ignores.length} rules in .vercelignore`);
|
||||
debug('Building file tree...');
|
||||
@@ -119,37 +122,50 @@ export async function buildFileTree(
|
||||
|
||||
export async function getVercelIgnore(
|
||||
cwd: string | string[],
|
||||
prebuilt?: boolean
|
||||
prebuilt?: boolean,
|
||||
rootDirectory?: string
|
||||
): Promise<{ ig: Ignore; ignores: string[] }> {
|
||||
const ignores: string[] = prebuilt
|
||||
? ['*', '!.output', '!.output/**']
|
||||
: [
|
||||
'.hg',
|
||||
'.git',
|
||||
'.gitmodules',
|
||||
'.svn',
|
||||
'.cache',
|
||||
'.next',
|
||||
'.now',
|
||||
'.vercel',
|
||||
'.npmignore',
|
||||
'.dockerignore',
|
||||
'.gitignore',
|
||||
'.*.swp',
|
||||
'.DS_Store',
|
||||
'.wafpicke-*',
|
||||
'.lock-wscript',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'.venv',
|
||||
'npm-debug.log',
|
||||
'config.gypi',
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'venv',
|
||||
'CVS',
|
||||
'.output',
|
||||
];
|
||||
let ignores: string[] = [];
|
||||
|
||||
const outputDir = posix.join(rootDirectory || '', '.output');
|
||||
|
||||
if (prebuilt) {
|
||||
ignores.push('*');
|
||||
const parts = outputDir.split('/');
|
||||
parts.forEach((_, i) => {
|
||||
const level = parts.slice(0, i + 1).join('/');
|
||||
ignores.push(`!${level}`);
|
||||
});
|
||||
ignores.push(`!${outputDir}/**`);
|
||||
} else {
|
||||
ignores = [
|
||||
'.hg',
|
||||
'.git',
|
||||
'.gitmodules',
|
||||
'.svn',
|
||||
'.cache',
|
||||
'.next',
|
||||
'.now',
|
||||
'.vercel',
|
||||
'.npmignore',
|
||||
'.dockerignore',
|
||||
'.gitignore',
|
||||
'.*.swp',
|
||||
'.DS_Store',
|
||||
'.wafpicke-*',
|
||||
'.lock-wscript',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'.venv',
|
||||
'npm-debug.log',
|
||||
'config.gypi',
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'venv',
|
||||
'CVS',
|
||||
`.output`,
|
||||
];
|
||||
}
|
||||
const cwds = Array.isArray(cwd) ? cwd : [cwd];
|
||||
|
||||
const files = await Promise.all(
|
||||
@@ -250,39 +266,31 @@ export const prepareFiles = (
|
||||
files: Map<string, DeploymentFile>,
|
||||
clientOptions: VercelClientOptions
|
||||
): PreparedFile[] => {
|
||||
const preparedFiles = [...files.keys()].reduce(
|
||||
(acc: PreparedFile[], sha: string): PreparedFile[] => {
|
||||
const next = [...acc];
|
||||
const preparedFiles: PreparedFile[] = [];
|
||||
for (const [sha, file] of files) {
|
||||
for (const name of file.names) {
|
||||
let fileName: string;
|
||||
|
||||
const file = files.get(sha) as DeploymentFile;
|
||||
|
||||
for (const name of file.names) {
|
||||
let fileName: string;
|
||||
|
||||
if (clientOptions.isDirectory) {
|
||||
// Directory
|
||||
fileName =
|
||||
typeof clientOptions.path === 'string'
|
||||
? relative(clientOptions.path, name)
|
||||
: name;
|
||||
} else {
|
||||
// Array of files or single file
|
||||
const segments = name.split(sep);
|
||||
fileName = segments[segments.length - 1];
|
||||
}
|
||||
|
||||
next.push({
|
||||
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
|
||||
size: file.data.byteLength || file.data.length,
|
||||
mode: file.mode,
|
||||
sha,
|
||||
});
|
||||
if (clientOptions.isDirectory) {
|
||||
// Directory
|
||||
fileName =
|
||||
typeof clientOptions.path === 'string'
|
||||
? relative(clientOptions.path, name)
|
||||
: name;
|
||||
} else {
|
||||
// Array of files or single file
|
||||
const segments = name.split(sep);
|
||||
fileName = segments[segments.length - 1];
|
||||
}
|
||||
|
||||
return next;
|
||||
},
|
||||
[]
|
||||
);
|
||||
preparedFiles.push({
|
||||
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
|
||||
size: file.data.byteLength || file.data.length,
|
||||
mode: file.mode,
|
||||
sha,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return preparedFiles;
|
||||
};
|
||||
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/foo.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/foo.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/baz.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/baz.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
baz
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/sub/qux.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/root/.output/sub/qux.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
qux
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/root/bar.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/root/bar.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bar
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/.output/baz.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/.output/baz.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
baz
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/.output/sub/qux.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/.output/sub/qux.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
qux
|
||||
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/bar.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api-root-directory/someother/bar.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bar
|
||||
1
packages/client/tests/fixtures/file-system-api/.output/baz.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api/.output/baz.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
baz
|
||||
1
packages/client/tests/fixtures/file-system-api/.output/sub/qux.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api/.output/sub/qux.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
qux
|
||||
1
packages/client/tests/fixtures/file-system-api/foo.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api/foo.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
foo
|
||||
1
packages/client/tests/fixtures/file-system-api/sub/bar.txt
vendored
Normal file
1
packages/client/tests/fixtures/file-system-api/sub/bar.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bar
|
||||
4
packages/client/tests/tsconfig.json
Normal file
4
packages/client/tests/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["*.test.ts"]
|
||||
}
|
||||
@@ -17,7 +17,11 @@ const toAbsolutePaths = (cwd: string, files: string[]) =>
|
||||
describe('buildFileTree()', () => {
|
||||
it('should exclude files using `.nowignore` blocklist', async () => {
|
||||
const cwd = fixture('nowignore');
|
||||
const { fileList, ignoreList } = await buildFileTree(cwd, true, noop);
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true },
|
||||
noop
|
||||
);
|
||||
|
||||
const expectedFileList = toAbsolutePaths(cwd, ['.nowignore', 'index.txt']);
|
||||
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
|
||||
@@ -36,7 +40,11 @@ describe('buildFileTree()', () => {
|
||||
|
||||
it('should include the node_modules using `.vercelignore` allowlist', async () => {
|
||||
const cwd = fixture('vercelignore-allow-nodemodules');
|
||||
const { fileList, ignoreList } = await buildFileTree(cwd, true, noop);
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true },
|
||||
noop
|
||||
);
|
||||
|
||||
const expected = toAbsolutePaths(cwd, [
|
||||
'node_modules/one.txt',
|
||||
@@ -54,4 +62,90 @@ describe('buildFileTree()', () => {
|
||||
normalizeWindowsPaths(ignoreList).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should find root files but ignore .output files when prebuilt=false', async () => {
|
||||
const cwd = fixture('file-system-api');
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true, prebuilt: false },
|
||||
noop
|
||||
);
|
||||
|
||||
const expectedFileList = toAbsolutePaths(cwd, ['foo.txt', 'sub/bar.txt']);
|
||||
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
|
||||
normalizeWindowsPaths(fileList).sort()
|
||||
);
|
||||
|
||||
const expectedIgnoreList = ['.output'];
|
||||
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
|
||||
normalizeWindowsPaths(ignoreList).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should find .output files but ignore other files when prebuilt=true', async () => {
|
||||
const cwd = fixture('file-system-api');
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true, prebuilt: true },
|
||||
noop
|
||||
);
|
||||
|
||||
const expectedFileList = toAbsolutePaths(cwd, [
|
||||
'.output/baz.txt',
|
||||
'.output/sub/qux.txt',
|
||||
]);
|
||||
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
|
||||
normalizeWindowsPaths(fileList).sort()
|
||||
);
|
||||
|
||||
const expectedIgnoreList = ['foo.txt', 'sub'];
|
||||
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
|
||||
normalizeWindowsPaths(ignoreList).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should find root files but ignore all .output files when prebuilt=false and rootDirectory=root', async () => {
|
||||
const cwd = fixture('file-system-api-root-directory');
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true, prebuilt: false, rootDirectory: 'root' },
|
||||
noop
|
||||
);
|
||||
|
||||
const expectedFileList = toAbsolutePaths(cwd, [
|
||||
'foo.txt',
|
||||
'root/bar.txt',
|
||||
'someother/bar.txt',
|
||||
]);
|
||||
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
|
||||
normalizeWindowsPaths(fileList).sort()
|
||||
);
|
||||
|
||||
const expectedIgnoreList = ['root/.output', 'someother/.output'];
|
||||
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
|
||||
normalizeWindowsPaths(ignoreList).sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should find root/.output files but ignore other files when prebuilt=true and rootDirectory=root', async () => {
|
||||
const cwd = fixture('file-system-api-root-directory');
|
||||
const { fileList, ignoreList } = await buildFileTree(
|
||||
cwd,
|
||||
{ isDirectory: true, prebuilt: true, rootDirectory: 'root' },
|
||||
noop
|
||||
);
|
||||
|
||||
const expectedFileList = toAbsolutePaths(cwd, [
|
||||
'root/.output/baz.txt',
|
||||
'root/.output/sub/qux.txt',
|
||||
]);
|
||||
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
|
||||
normalizeWindowsPaths(fileList).sort()
|
||||
);
|
||||
|
||||
const expectedIgnoreList = ['foo.txt', 'root/bar.txt', 'someother'];
|
||||
expect(normalizeWindowsPaths(expectedIgnoreList).sort()).toEqual(
|
||||
normalizeWindowsPaths(ignoreList).sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel-plugin-middleware",
|
||||
"version": "0.0.0-canary.15",
|
||||
"version": "0.0.0-canary.21",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@types/node-fetch": "^2",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"cookie": "0.4.1",
|
||||
"formdata-node": "4.3.1",
|
||||
|
||||
52
packages/middleware/src/esbuild-plugins.ts
Normal file
52
packages/middleware/src/esbuild-plugins.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import path from 'path';
|
||||
import * as esbuild from 'esbuild';
|
||||
|
||||
const processInjectFile = `
|
||||
// envOverride is passed by esbuild plugin
|
||||
const env = envOverride
|
||||
function cwd() {
|
||||
return '/'
|
||||
}
|
||||
function chdir(dir) {
|
||||
throw new Error('process.chdir is not supported')
|
||||
}
|
||||
export const process = {
|
||||
argv: [],
|
||||
env,
|
||||
chdir,
|
||||
cwd,
|
||||
};
|
||||
`;
|
||||
|
||||
export function nodeProcessPolyfillPlugin({ env = {} } = {}): esbuild.Plugin {
|
||||
return {
|
||||
name: 'node-process-polyfill',
|
||||
setup({ initialOptions, onResolve, onLoad }) {
|
||||
onResolve({ filter: /_virtual-process-polyfill_\.js/ }, ({ path }) => {
|
||||
return {
|
||||
path,
|
||||
sideEffects: false,
|
||||
};
|
||||
});
|
||||
|
||||
onLoad({ filter: /_virtual-process-polyfill_\.js/ }, () => {
|
||||
const contents = `const envOverride = ${JSON.stringify(
|
||||
env
|
||||
)};\n${processInjectFile}`;
|
||||
return {
|
||||
loader: 'js',
|
||||
contents,
|
||||
};
|
||||
});
|
||||
|
||||
const polyfills = [
|
||||
path.resolve(__dirname, '_virtual-process-polyfill_.js'),
|
||||
];
|
||||
if (initialOptions.inject) {
|
||||
initialOptions.inject.push(...polyfills);
|
||||
} else {
|
||||
initialOptions.inject = [...polyfills];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
UrlWithParsedQuery,
|
||||
} from 'url';
|
||||
import { toNodeHeaders } from './websandbox/utils';
|
||||
import { nodeProcessPolyfillPlugin } from './esbuild-plugins';
|
||||
|
||||
const glob = util.promisify(libGlob);
|
||||
const SUPPORTED_EXTENSIONS = ['.js', '.ts'];
|
||||
@@ -80,6 +81,7 @@ export async function build({ workPath }: { workPath: string }) {
|
||||
banner: {
|
||||
js: '"use strict";',
|
||||
},
|
||||
plugins: [nodeProcessPolyfillPlugin({ env: process.env })],
|
||||
format: 'cjs',
|
||||
});
|
||||
// Create `_ENTRIES` wrapper
|
||||
|
||||
@@ -15,6 +15,6 @@ Object {
|
||||
"sortingIndex": 1,
|
||||
},
|
||||
},
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
}
|
||||
`;
|
||||
|
||||
26
packages/middleware/test/build.test.ts
vendored
26
packages/middleware/test/build.test.ts
vendored
@@ -63,6 +63,32 @@ describe('build()', () => {
|
||||
).toEqual('1');
|
||||
});
|
||||
|
||||
it('should build simple middleware with env vars', async () => {
|
||||
const expectedEnvVar = 'expected-env-var';
|
||||
const fixture = join(__dirname, 'fixtures/env');
|
||||
process.env.ENV_VAR_SHOULD_BE_DEFINED = expectedEnvVar;
|
||||
await build({
|
||||
workPath: fixture,
|
||||
});
|
||||
// env var should be inlined in the output
|
||||
delete process.env.ENV_VAR_SHOULD_BE_DEFINED;
|
||||
|
||||
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
|
||||
expect(await fsp.stat(outputFile)).toBeTruthy();
|
||||
|
||||
require(outputFile);
|
||||
//@ts-ignore
|
||||
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
|
||||
expect(typeof middleware).toStrictEqual('function');
|
||||
const handledResponse = await middleware({
|
||||
request: {},
|
||||
});
|
||||
expect(String(handledResponse.response.body)).toEqual(expectedEnvVar);
|
||||
expect(
|
||||
(handledResponse.response as Response).headers.get('x-middleware-next')
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('should create a middleware that runs in strict mode', async () => {
|
||||
const { middleware } = await setupFixture('use-strict');
|
||||
const response = await middleware({
|
||||
|
||||
3
packages/middleware/test/fixtures/env/_middleware.js
vendored
Normal file
3
packages/middleware/test/fixtures/env/_middleware.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default req => {
|
||||
return new Response(process.env.ENV_VAR_SHOULD_BE_DEFINED);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vercel-plugin-go",
|
||||
"version": "1.0.0-canary.27",
|
||||
"version": "1.0.0-canary.32",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
@@ -17,7 +17,7 @@
|
||||
"prepublishOnly": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/go": "1.2.4-canary.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel-plugin-node",
|
||||
"version": "1.12.2-canary.31",
|
||||
"version": "1.12.2-canary.36",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@types/node-fetch": "2",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"@types/yazl": "2.4.2",
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/fun": "1.0.3",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.14.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vercel-plugin-python",
|
||||
"version": "1.0.0-canary.28",
|
||||
"version": "1.0.0-canary.33",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
@@ -17,8 +17,8 @@
|
||||
"prepublishOnly": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/python": "2.1.2-canary.1"
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/python": "2.1.2-canary.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vercel-plugin-ruby",
|
||||
"version": "1.0.0-canary.27",
|
||||
"version": "1.0.0-canary.32",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
@@ -17,7 +17,7 @@
|
||||
"prepublishOnly": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.12.3-canary.39",
|
||||
"@vercel/build-utils": "2.12.3-canary.44",
|
||||
"@vercel/ruby": "1.2.10-canary.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/python",
|
||||
"version": "2.1.2-canary.1",
|
||||
"version": "2.1.2-canary.2",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { relative, basename } from 'path';
|
||||
import execa from 'execa';
|
||||
import { Meta, debug } from '@vercel/build-utils';
|
||||
|
||||
@@ -136,17 +135,10 @@ export async function installRequirementsFile({
|
||||
meta,
|
||||
args = [],
|
||||
}: InstallRequirementsFileArg) {
|
||||
const fileAtRoot = relative(workPath, filePath) === basename(filePath);
|
||||
|
||||
// If the `requirements.txt` file is located in the Root Directory of the project and
|
||||
// the new File System API is used (`avoidTopLevelInstall`), the Install Command
|
||||
// will have already installed its dependencies, so we don't need to do it again.
|
||||
if (meta.avoidTopLevelInstall && fileAtRoot) {
|
||||
debug(
|
||||
`Skipping requirements file installation, already installed by Install Command`
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The Vercel platform already handles `requirements.txt` for frontend projects,
|
||||
// but the installation logic there is different, because it seems to install all
|
||||
// of the dependencies globally, whereas, for this Runtime, we want it to happen only
|
||||
// locally, so we'll run a separate installation.
|
||||
|
||||
if (
|
||||
meta.isDev &&
|
||||
|
||||
Reference in New Issue
Block a user