mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
[static-build] Add support for Build Output v3 detection (#7669)
Adds detection logic for a framework / build script outputting Build Output v3 format to the filesystem. In this case, `static-build` will simply stop processing after the Build Command since deserialization happens in the build-container side of things (this is different compared to the v1 output which gets handled in this Builder. The reason for that is because the v3 output matches what `vc build` outputs vs. v1 which is a different format).
This commit is contained in:
@@ -356,7 +356,29 @@ export interface Images {
|
||||
formats?: ImageFormat[];
|
||||
}
|
||||
|
||||
export interface BuildResultV2 {
|
||||
/**
|
||||
* If a Builder ends up creating filesystem outputs conforming to
|
||||
* the Build Output API, then the Builder should return this type.
|
||||
*/
|
||||
export interface BuildResultBuildOutput {
|
||||
/**
|
||||
* Version number of the Build Output API that was created.
|
||||
* Currently only `3` is a valid value.
|
||||
* @example 3
|
||||
*/
|
||||
buildOutputVersion: 3;
|
||||
/**
|
||||
* Filesystem path to the Build Output directory.
|
||||
* @example "/path/to/.vercel/output"
|
||||
*/
|
||||
buildOutputPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a Builder implements `version: 2`, the `build()` function is expected
|
||||
* to return this type.
|
||||
*/
|
||||
export interface BuildResultV2Typical {
|
||||
// TODO: use proper `Route` type from `routing-utils` (perhaps move types to a common package)
|
||||
routes?: any[];
|
||||
images?: Images;
|
||||
@@ -369,6 +391,8 @@ export interface BuildResultV2 {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type BuildResultV2 = BuildResultV2Typical | BuildResultBuildOutput;
|
||||
|
||||
export interface BuildResultV3 {
|
||||
output: Lambda;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,24 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build",
|
||||
"test-unit": "jest --env node --verbose --runInBand --bail test/unit.test.js",
|
||||
"test-unit": "jest --env node --verbose --bail test/build.test.ts test/prepare-cache.test.ts",
|
||||
"test-integration-once": "jest --env node --verbose --runInBand --bail test/integration.test.js",
|
||||
"prepublishOnly": "node build"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/default",
|
||||
"testEnvironment": "node",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"diagnostics": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/aws-lambda": "8.10.64",
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/ms": "0.7.31",
|
||||
"@types/node-fetch": "2.5.4",
|
||||
"@types/promise-timeout": "1.3.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import fetch from 'node-fetch';
|
||||
import getPort from 'get-port';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import frameworks, { Framework } from '@vercel/frameworks';
|
||||
import { ChildProcess, SpawnOptions } from 'child_process';
|
||||
import type { ChildProcess, SpawnOptions } from 'child_process';
|
||||
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
||||
import { cpus } from 'os';
|
||||
import {
|
||||
@@ -30,14 +30,12 @@ import {
|
||||
NowBuildError,
|
||||
scanParentDirs,
|
||||
} from '@vercel/build-utils';
|
||||
import { Route, Source } from '@vercel/routing-utils';
|
||||
import {
|
||||
readBuildOutputDirectory,
|
||||
readBuildOutputConfig,
|
||||
} from './utils/read-build-output';
|
||||
import type { Route, Source } from '@vercel/routing-utils';
|
||||
import * as BuildOutputV1 from './utils/build-output-v1';
|
||||
import * as BuildOutputV3 from './utils/build-output-v3';
|
||||
import * as GatsbyUtils from './utils/gatsby';
|
||||
import * as NuxtUtils from './utils/nuxt';
|
||||
import { ImagesConfig, BuildConfig } from './utils/_shared';
|
||||
import type { ImagesConfig, BuildConfig } from './utils/_shared';
|
||||
|
||||
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
|
||||
|
||||
@@ -284,11 +282,8 @@ export const build: BuildV2 = async ({
|
||||
);
|
||||
|
||||
const pkg = getPkg(entrypoint, workPath);
|
||||
|
||||
const devScript = pkg ? getScriptName(pkg, 'dev', config) : null;
|
||||
|
||||
const framework = getFramework(config, pkg);
|
||||
|
||||
const devCommand = getCommand('dev', pkg, config, framework);
|
||||
const buildCommand = getCommand('build', pkg, config, framework);
|
||||
const installCommand = getCommand('install', pkg, config, framework);
|
||||
@@ -637,6 +632,30 @@ export const build: BuildV2 = async ({
|
||||
|
||||
const outputDirPrefix = path.join(workPath, path.dirname(entrypoint));
|
||||
|
||||
// If the Build Command or Framework output files according to the
|
||||
// Build Output v3 API, then stop processing here in `static-build`
|
||||
// since the output is already in its final form.
|
||||
const buildOutputPath = await BuildOutputV3.getBuildOutputDirectory(
|
||||
outputDirPrefix
|
||||
);
|
||||
|
||||
if (buildOutputPath) {
|
||||
// Ensure that `vercel build` is being used for this Deployment
|
||||
if (!meta.cliVersion) {
|
||||
let buildCommandName: string;
|
||||
if (buildCommand) buildCommandName = `"${buildCommand}"`;
|
||||
else if (framework) buildCommandName = framework.name;
|
||||
else buildCommandName = 'the "build" script';
|
||||
throw new Error(
|
||||
`Detected Build Output v3 from ${buildCommandName}, but this Deployment is not using \`vercel build\`.\nPlease set the \`ENABLE_VC_BUILD=1\` environment variable.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
buildOutputVersion: 3,
|
||||
buildOutputPath,
|
||||
};
|
||||
}
|
||||
|
||||
if (framework) {
|
||||
const outputDirName = config.outputDirectory
|
||||
? config.outputDirectory
|
||||
@@ -656,7 +675,7 @@ export const build: BuildV2 = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const extraOutputs = await readBuildOutputDirectory({
|
||||
const extraOutputs = await BuildOutputV1.readBuildOutputDirectory({
|
||||
workPath,
|
||||
nodeVersion,
|
||||
});
|
||||
@@ -750,7 +769,7 @@ export const prepareCache: PrepareCache = async ({
|
||||
const cacheFiles: Files = {};
|
||||
|
||||
// File System API v1 cache files
|
||||
const buildConfig = await readBuildOutputConfig<BuildConfig>({
|
||||
const buildConfig = await BuildOutputV1.readBuildOutputConfig<BuildConfig>({
|
||||
workPath,
|
||||
configFileName: 'build.json',
|
||||
});
|
||||
|
||||
23
packages/static-build/src/utils/build-output-v3.ts
Normal file
23
packages/static-build/src/utils/build-output-v3.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { join } from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const BUILD_OUTPUT_DIR = '.vercel/output';
|
||||
|
||||
/**
|
||||
* Returns the path to the Build Output v3 directory when the
|
||||
* `config.json` file was created by the framework / build script,
|
||||
* or `undefined` if the framework did not create the v3 output.
|
||||
*/
|
||||
export async function getBuildOutputDirectory(
|
||||
path: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const outputDir = join(path, BUILD_OUTPUT_DIR);
|
||||
const configPath = join(outputDir, 'config.json');
|
||||
await fs.stat(configPath);
|
||||
return outputDir;
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -30,9 +30,7 @@ export async function injectVercelAnalyticsPlugin(dir: string) {
|
||||
`Injecting Gatsby.js analytics plugin "${gatsbyPluginPackageName}" to \`${gatsbyConfigPath}\``
|
||||
);
|
||||
|
||||
const pkgJson: DeepWriteable<PackageJson> = (await readPackageJson(
|
||||
dir
|
||||
)) as DeepWriteable<PackageJson>;
|
||||
const pkgJson = (await readPackageJson(dir)) as DeepWriteable<PackageJson>;
|
||||
if (!pkgJson.dependencies) {
|
||||
pkgJson.dependencies = {};
|
||||
}
|
||||
|
||||
7
packages/static-build/test/build-fixtures/09-build-output-v3/build.js
Executable file
7
packages/static-build/test/build-fixtures/09-build-output-v3/build.js
Executable file
@@ -0,0 +1,7 @@
|
||||
const fs = require('fs');
|
||||
fs.mkdirSync('.vercel/output/static', { recursive: true });
|
||||
fs.writeFileSync('.vercel/output/config.json', '{}');
|
||||
fs.writeFileSync(
|
||||
'.vercel/output/static/index.html',
|
||||
'<h1>Build Output API</h1>'
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "09-build-output-v3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
}
|
||||
}
|
||||
54
packages/static-build/test/build.test.ts
vendored
Normal file
54
packages/static-build/test/build.test.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import path from 'path';
|
||||
import { build } from '../src';
|
||||
|
||||
describe('build()', () => {
|
||||
it('should detect Builder Output v3', async () => {
|
||||
const workPath = path.join(
|
||||
__dirname,
|
||||
'build-fixtures',
|
||||
'09-build-output-v3'
|
||||
);
|
||||
const buildResult = await build({
|
||||
files: {},
|
||||
entrypoint: 'package.json',
|
||||
workPath,
|
||||
config: {},
|
||||
meta: {
|
||||
skipDownload: true,
|
||||
cliVersion: '0.0.0',
|
||||
},
|
||||
});
|
||||
if ('output' in buildResult) {
|
||||
throw new Error('Unexpected `output` in build result');
|
||||
}
|
||||
expect(buildResult.buildOutputVersion).toEqual(3);
|
||||
expect(buildResult.buildOutputPath).toEqual(
|
||||
path.join(workPath, '.vercel/output')
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an Error with Builder Output v3 without `vercel build`', async () => {
|
||||
let err;
|
||||
const workPath = path.join(
|
||||
__dirname,
|
||||
'build-fixtures',
|
||||
'09-build-output-v3'
|
||||
);
|
||||
try {
|
||||
await build({
|
||||
files: {},
|
||||
entrypoint: 'package.json',
|
||||
workPath,
|
||||
config: {},
|
||||
meta: {
|
||||
skipDownload: true,
|
||||
},
|
||||
});
|
||||
} catch (_err: any) {
|
||||
err = _err;
|
||||
}
|
||||
expect(err.message).toEqual(
|
||||
`Detected Build Output v3 from the "build" script, but this Deployment is not using \`vercel build\`.\nPlease set the \`ENABLE_VC_BUILD=1\` environment variable.`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
const { prepareCache } = require('../dist');
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
import { prepareCache } from '../src';
|
||||
|
||||
describe('prepareCache', () => {
|
||||
describe('prepareCache()', () => {
|
||||
test('should cache node_modules and .shadow-cljs', async () => {
|
||||
const files = await prepareCache({
|
||||
config: { zeroConfig: true },
|
||||
workPath: path.resolve(__dirname, './cache-fixtures/default'),
|
||||
entrypoint: 'index.js',
|
||||
files: {},
|
||||
});
|
||||
|
||||
expect(files['node_modules/file']).toBeDefined();
|
||||
@@ -19,6 +20,7 @@ describe('prepareCache', () => {
|
||||
config: { zeroConfig: true },
|
||||
workPath: path.resolve(__dirname, './cache-fixtures/withCacheConfig'),
|
||||
entrypoint: 'index.js',
|
||||
files: {},
|
||||
});
|
||||
|
||||
expect(files['node_modules/file']).toBeUndefined();
|
||||
@@ -32,6 +34,7 @@ describe('prepareCache', () => {
|
||||
config: { zeroConfig: true },
|
||||
workPath: path.resolve(__dirname, './cache-fixtures/gatsby'),
|
||||
entrypoint: 'package.json',
|
||||
files: {},
|
||||
});
|
||||
|
||||
expect(files['node_modules/file2']).toBeDefined();
|
||||
@@ -45,6 +48,7 @@ describe('prepareCache', () => {
|
||||
config: { zeroConfig: true, framework: 'jekyll' },
|
||||
workPath: path.resolve(__dirname, './cache-fixtures/jekyll'),
|
||||
entrypoint: 'Gemfile',
|
||||
files: {},
|
||||
});
|
||||
|
||||
expect(files['vendor/bundle/b1']).toBeDefined();
|
||||
4
packages/static-build/test/tsconfig.json
vendored
Normal file
4
packages/static-build/test/tsconfig.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["*.test.ts"]
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "dist",
|
||||
"types": ["node"],
|
||||
"types": ["node", "jest"],
|
||||
"strict": true,
|
||||
"target": "es2018"
|
||||
},
|
||||
|
||||
47
yarn.lock
47
yarn.lock
@@ -2180,6 +2180,14 @@
|
||||
jest-diff "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/jest@27.4.1":
|
||||
version "27.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
|
||||
integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
|
||||
dependencies:
|
||||
jest-matcher-utils "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
|
||||
"@types/js-yaml@3.12.1":
|
||||
version "3.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656"
|
||||
@@ -4677,6 +4685,11 @@ diff-sequences@^27.0.6:
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
|
||||
integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
|
||||
|
||||
diff-sequences@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
|
||||
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
@@ -7160,6 +7173,16 @@ jest-diff@^27.3.1:
|
||||
jest-get-type "^27.3.1"
|
||||
pretty-format "^27.3.1"
|
||||
|
||||
jest-diff@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
||||
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
diff-sequences "^27.5.1"
|
||||
jest-get-type "^27.5.1"
|
||||
pretty-format "^27.5.1"
|
||||
|
||||
jest-docblock@^27.0.6:
|
||||
version "27.0.6"
|
||||
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3"
|
||||
@@ -7213,6 +7236,11 @@ jest-get-type@^27.3.1:
|
||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff"
|
||||
integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg==
|
||||
|
||||
jest-get-type@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
||||
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
|
||||
|
||||
jest-haste-map@^27.3.1:
|
||||
version "27.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee"
|
||||
@@ -7265,6 +7293,16 @@ jest-leak-detector@^27.3.1:
|
||||
jest-get-type "^27.3.1"
|
||||
pretty-format "^27.3.1"
|
||||
|
||||
jest-matcher-utils@^27.0.0:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
|
||||
integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
jest-diff "^27.5.1"
|
||||
jest-get-type "^27.5.1"
|
||||
pretty-format "^27.5.1"
|
||||
|
||||
jest-matcher-utils@^27.3.1:
|
||||
version "27.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.3.1.tgz#257ad61e54a6d4044e080d85dbdc4a08811e9c1c"
|
||||
@@ -9540,6 +9578,15 @@ pretty-format@^27.3.1:
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-ms@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.1.0.tgz#b906bdd1ec9e9799995c372e2b1c34f073f95384"
|
||||
|
||||
Reference in New Issue
Block a user