Compare commits

..

37 Commits

Author SHA1 Message Date
Ethan Arrowood
92f5b6e0c9 Publish Stable
- vercel@28.13.0
 - @vercel/gatsby-plugin-vercel-builder@0.1.2
 - @vercel/next@3.3.18
 - @vercel/node@2.8.15
 - @vercel/static-build@1.2.0
2023-01-20 10:27:31 -07:00
JJ Kasper
ed6ce1149a [next] Ensure outputDirectory is normalized (#9269)
This ensures we normalize an absolute `outputDirectory` passed into the Next.js builder as this can cause incorrect resolving. We should probably normalize this higher up as well but this is a starting point. 

Fixes: [slack thread](https://vercel.slack.com/archives/C0289CGVAR2/p1674176040821579?thread_ts=1674168748.257019&cid=C0289CGVAR2)
2023-01-20 17:08:32 +00:00
Ethan Arrowood
fc3611fb80 [static-build] add gatsby v5 fixture (#9267)
Adds a gatsby v5 fixture using ssr and dsg to demonstrate plugin works

Co-authored-by: Nathan Rajlich <n@n8.io>
2023-01-20 03:02:26 -08:00
Nathan Rajlich
ed33c2b27c [gatsby-plugin-vercel-builder] Remove some leftover console.log calls (#9265) 2023-01-19 16:05:48 -08:00
Nathan Rajlich
a7a5bf1a12 [tests] Pass in builder to runBuildLambda() (#9262)
Removes the need for `next`/`static-build` to be present in the root `package.json` file.
2023-01-19 15:10:06 -08:00
JJ Kasper
cc687b3880 [next] Correct RSC route for app dir with trailingSlash (#9263) 2023-01-19 15:03:58 -08:00
Nathan Rajlich
053ec92d5f [node] Add build logs probe for TypeScript usage messages (#9261)
Follow-up to https://github.com/vercel/vercel/pull/9258, even though that issue seemed to only happen when linked to the monorepo locally. In any case, this test will ensure those log messages are intact for any other change around that part of the codebase in the future.
2023-01-19 21:39:27 +00:00
Ethan Arrowood
4838dc336a [static-build] run pnpm install --lockfile-only after injecting gatsby plugins (#9259)
Fixes a bug where after injecting plugins and modifying the users package.json, pnpm would fail normal install
2023-01-19 20:07:53 +00:00
Nathan Rajlich
eae45f4019 [node] Fix TypeScript built-in compiler log message check (#9258)
This check started breaking probably after the pnpm switch, so switch to a simpler solution that doesn't require comparing the paths.
2023-01-19 13:12:31 +00:00
Ethan Arrowood
02feb564a7 [static-build][fs-detectors] add gatsby builder plugin injection (#9252)
Modifies the `@vercel/static-build` injection logic in order to now inject both the analytics and builder gatsby plugins.
2023-01-19 03:34:47 +00:00
Nathan Rajlich
e174a06673 Publish Stable
- vercel@28.12.8
 - @vercel/gatsby-plugin-vercel-builder@0.1.1
 - @vercel/next@3.3.17
 - @vercel/remix@1.2.7
2023-01-18 15:19:12 -08:00
Nathan Rajlich
de034943af [gatsby-plugin-vercel-builder] Fix turbo cache outputs configuration (#9257)
The published version on npm was missing the compiled code because Turbo
was not configured to cache them properly.
2023-01-18 15:10:28 -08:00
JJ Kasper
b3862271a5 [next] Fix normalize route with middleware and basePath (#9255)
This ensures we properly match the index route when normalizing routes with `x-nextjs-data` for middleware. 

x-ref: [slack thread](https://vercel.slack.com/archives/C03S8ED1DKM/p1674034533265989)
2023-01-18 21:30:30 +00:00
Steven
aaceeef604 [tests] Add test for ./examples/remix (#9251)
- Related to #9249
2023-01-18 19:13:55 +00:00
Steven
ad107ecf79 [tests] Split more dev tests (#9230)
This PR attempts to balance the tests so they run concurrently and therefore faster.

I also sorted the tests so they are deterministic when splitting/chunking.
2023-01-18 17:35:45 +00:00
Steven
79ef5c3724 Publish Stable
- vercel@28.12.7
 - @vercel/client@12.3.2
 - @vercel/gatsby-plugin-vercel-builder@0.1.0
 - @vercel/next@3.3.16
 - @vercel/node-bridge@3.1.10
 - @vercel/node@2.8.14
 - @vercel/remix@1.2.6
2023-01-18 08:39:44 -05:00
Nathan Rajlich
02ff265074 [remix] Fix config file dynamic import (#9249)
Follow-up to https://github.com/vercel/vercel/pull/8793, which breaks dynamic import of the config file, causing some issues for remix users: https://github.com/orgs/vercel/discussions/1282

The underlying error is `MODULE_NOT_FOUND`.

`import()` should work with `file://` URIs, however, since it's using typescript, the `import` gets compiled to `require()`, which does not support `file://` protocol scheme.

Supersedes https://github.com/vercel/vercel/pull/9248.
2023-01-18 11:24:42 +00:00
Ethan Arrowood
ae89b8b8be [gatsby-plugin-vercel-builder] Implement @vercel/gatsby-plugin-vercel-builder (#9218)
<picture data-single-emoji=":gatsby:" title=":gatsby:"><img class="emoji" src="https://single-emoji.vercel.app/api/emoji/eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Dli3tTuity9MEBnK.YyaiT9ASg3XvFmlx5q0Ovkdbkto2fgJGjIJhsLcraR_hqYG0DAC6CcBMiaARcI_hF0502EnqhkrHQLeYfEoxfomLi9iKk4WBAe-oSfsENsG9oAwzYdH4LA.VMtHCMaOXNOqpB2xvph3Kg" alt=":gatsby:" width="20" height="auto" align="absmiddle"></picture> 

Implement the Build Output API v3 Gatsby plugin for use within `@vercel/static-build`.

Supersedes https://github.com/vercel/vercel/pull/8259.
2023-01-18 02:24:32 +00:00
Steven
4ccdcde463 Publish Stable
- @vercel/build-utils@5.9.0
 - vercel@28.12.6
 - @vercel/client@12.3.1
 - @vercel/fs-detectors@3.7.5
 - @vercel/go@2.2.30
 - @vercel/hydrogen@0.0.44
 - @vercel/next@3.3.15
 - @vercel/node@2.8.13
 - @vercel/python@3.1.40
 - @vercel/redwood@1.0.51
 - @vercel/remix@1.2.5
 - @vercel/ruby@1.3.56
 - @vercel/static-build@1.1.7
2023-01-17 19:25:26 -05:00
Nathan Rajlich
22d3ee160b [build-utils] Add includeDirectories option to glob() (#9245)
Instead of always including empty directories in `glob()`, make it an
opt-in behavior because technically it's a breaking change to include
them by default.
2023-01-17 19:21:40 -05:00
Sean Massa
6d97e1673e Publish Stable
- vercel@28.12.5
 - @vercel/client@12.3.0
 - @vercel/node-bridge@3.1.9
 - @vercel/node@2.8.12
2023-01-17 13:46:58 -06:00
Nathan Rajlich
522565f6e5 [node-bridge] Add missing lazy dependencies (#9231)
These were missing from the compiled ncc build of `helpers.ts`, and thus causing an error at runtime because the deps are not available within the Serverless Function.
2023-01-14 01:41:50 +00:00
Steven
07bf81ab10 [client] Add sliding window delay when polling deployment complete (#9222)
The previous delay of 1500ms was causing some users to hit the API rate limits. This doesn't normally happen with a single deployment, but it can happen with several concurrent deployments (for example a monorepo with many projects).

We don't need to be polling so often, so this PR changed the polling delay to the following:

- During 0s-15s: check every 1 second
- During 15s-60s: check every 5 seconds
- During 1m-5m: check every 15 seconds
- During 5m-10m: check every 30 seconds
2023-01-14 00:27:14 +00:00
Steven
35024a4e3a [tests] Split up dev tests to increase concurrency (#9228)
This will speed up CI because we can run more tests concurrently.

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 18:53:57 -05:00
Sean Massa
c1df9bca19 [tests] skip private packages (#9229) 2023-01-13 17:24:50 -06:00
Nathan Rajlich
4c1cdd1f0f [client] Send empty directory entries to POST create deployment (#9118)
Update `@vercel/client` to send empty directory entries to the `POST` create deployment endpoint. This makes it so that CLI deployments will have empty directories re-populated in the build-container when doing `vc deploy`.

Follow-up to #9103.
2023-01-13 22:24:31 +00:00
Sean Massa
b5cdc82a1c Publish Stable
- @vercel/build-utils@5.8.3
 - vercel@28.12.4
 - @vercel/client@12.2.31
 - @vercel/edge@0.2.6
 - @vercel/error-utils@1.0.8
 - @vercel/frameworks@1.2.4
 - @vercel/fs-detectors@3.7.4
 - @vercel/gatsby-plugin-vercel-analytics@1.0.6
 - @vercel/go@2.2.29
 - @vercel/hydrogen@0.0.43
 - @vercel/next@3.3.14
 - @vercel/node-bridge@3.1.8
 - @vercel/node@2.8.11
 - @vercel/python@3.1.39
 - @vercel/redwood@1.0.50
 - @vercel/remix@1.2.4
 - @vercel/routing-utils@2.1.8
 - @vercel/ruby@1.3.55
 - @vercel/static-build@1.1.6
 - @vercel/static-config@2.0.11
2023-01-13 15:45:03 -06:00
Nathan Rajlich
c7851404b3 [*] Remove "workspace:" (#9225) 2023-01-13 15:42:29 -06:00
Sean Massa
e54da8a2e5 Publish Stable
- @vercel/build-utils@5.8.2
 - vercel@28.12.3
 - @vercel/client@12.2.30
 - @vercel/edge@0.2.5
 - @vercel/error-utils@1.0.7
 - @vercel/frameworks@1.2.3
 - @vercel/fs-detectors@3.7.3
 - @vercel/gatsby-plugin-vercel-analytics@1.0.5
 - @vercel/go@2.2.28
 - @vercel/hydrogen@0.0.42
 - @vercel/next@3.3.13
 - @vercel/node-bridge@3.1.7
 - @vercel/node@2.8.10
 - @vercel/python@3.1.38
 - @vercel/redwood@1.0.49
 - @vercel/remix@1.2.3
 - @vercel/routing-utils@2.1.7
 - @vercel/ruby@1.3.54
 - @vercel/static-build@1.1.5
 - @vercel/static-config@2.0.10
2023-01-13 15:06:45 -06:00
Sean Massa
a066bedf95 add the lockfile before commit 2023-01-13 15:06:16 -06:00
Sean Massa
09b23e53ba update lockfile because lerna did not do its job 2023-01-13 15:05:29 -06:00
Sean Massa
b793a67588 Publish Stable
- @vercel/build-utils@5.8.1
 - vercel@28.12.2
 - @vercel/client@12.2.29
 - @vercel/edge@0.2.4
 - @vercel/error-utils@1.0.6
 - @vercel/frameworks@1.2.2
 - @vercel/fs-detectors@3.7.2
 - @vercel/gatsby-plugin-vercel-analytics@1.0.4
 - @vercel/go@2.2.27
 - @vercel/hydrogen@0.0.41
 - @vercel/next@3.3.12
 - @vercel/node-bridge@3.1.6
 - @vercel/node@2.8.9
 - @vercel/python@3.1.37
 - @vercel/redwood@1.0.48
 - @vercel/remix@1.2.2
 - @vercel/routing-utils@2.1.6
 - @vercel/ruby@1.3.53
 - @vercel/static-build@1.1.4
 - @vercel/static-config@2.0.9
2023-01-13 15:01:55 -06:00
Sean Massa
31dd354b3a remove unnecessary checkout 2023-01-13 15:00:52 -06:00
Sean Massa
529ff3b2d7 add "version" lifecycle hook for lerna to update lockfile before commit 2023-01-13 14:59:49 -06:00
Sean Massa
e71d5638ee Publish Stable
- @vercel/build-utils@5.8.0
 - vercel@28.12.1
 - @vercel/client@12.2.28
 - @vercel/edge@0.2.3
 - @vercel/error-utils@1.0.5
 - @vercel/frameworks@1.2.1
 - @vercel/fs-detectors@3.7.1
 - @vercel/gatsby-plugin-vercel-analytics@1.0.3
 - @vercel/go@2.2.26
 - @vercel/hydrogen@0.0.40
 - @vercel/next@3.3.11
 - @vercel/node-bridge@3.1.5
 - @vercel/node@2.8.8
 - @vercel/python@3.1.36
 - @vercel/redwood@1.0.47
 - @vercel/remix@1.2.1
 - @vercel/routing-utils@2.1.5
 - @vercel/ruby@1.3.52
 - @vercel/static-build@1.1.3
 - @vercel/static-config@2.0.8
2023-01-13 14:47:25 -06:00
Ethan Arrowood
8c16e765ee [tests] adjust root and api deps to use worskpace:* (#9223) 2023-01-13 14:29:23 -06:00
Ethan Arrowood
a008c9c7fe [build-utils] Support empty directory entries for glob() and download() (#9164)
Reopening #9103 since it was reverted.
2023-01-13 12:01:37 -08:00
230 changed files with 51599 additions and 2373 deletions

View File

@@ -43,5 +43,8 @@ packages/static-build/test/cache-fixtures
# redwood
packages/redwood/test/fixtures
# remix
packages/remix/test/fixtures
# gatsby-plugin-vercel-analytics
packages/gatsby-plugin-vercel-analytics

View File

@@ -4,8 +4,7 @@
"version": "0.0.0",
"description": "API for the vercel/vercel repo",
"main": "index.js",
"scripts": {
},
"scripts": {},
"dependencies": {
"@sentry/node": "5.11.1",
"got": "10.2.1",
@@ -17,7 +16,7 @@
"devDependencies": {
"@types/node": "16.18.11",
"@types/node-fetch": "2.5.4",
"@vercel/node": "workspace:2.8.6",
"@vercel/node": "*",
"typescript": "4.3.4"
}
}

View File

@@ -11,9 +11,8 @@
"@types/node": "14.18.33",
"@typescript-eslint/eslint-plugin": "5.21.0",
"@typescript-eslint/parser": "5.21.0",
"@vercel/build-utils": "workspace:5.7.5",
"@vercel/build-utils": "*",
"@vercel/ncc": "0.24.0",
"@vercel/next": "workspace:3.3.9",
"async-retry": "1.2.3",
"buffer-replace": "1.0.0",
"create-svelte": "2.0.1",
@@ -37,6 +36,7 @@
},
"scripts": {
"lerna": "lerna",
"version": "pnpm install && git add pnpm-lock.yaml",
"bootstrap": "lerna bootstrap",
"publish-stable": "echo 'Run `pnpm changelog` for instructions'",
"publish-canary": "git checkout main && git pull && lerna version prerelease --preid canary --message \"Publish Canary\" --exact",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "5.7.6",
"version": "5.9.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -2,7 +2,7 @@ import path from 'path';
import debug from '../debug';
import FileFsRef from '../file-fs-ref';
import { File, Files, Meta } from '../types';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
import { remove, mkdirp, readlink, symlink, chmod } from 'fs-extra';
import streamToBuffer from './stream-to-buffer';
export interface DownloadedFiles {
@@ -51,6 +51,12 @@ export async function downloadFile(
): Promise<FileFsRef> {
const { mode } = file;
if (isDirectory(mode)) {
await mkdirp(fsPath);
await chmod(fsPath, mode);
return FileFsRef.fromFsPath({ mode, fsPath });
}
// If the source is a symlink, try to create it instead of copying the file.
// Note: creating symlinks on Windows requires admin priviliges or symlinks
// enabled in the group policy. We may want to improve the error message.

View File

@@ -6,7 +6,9 @@ import { lstat, Stats } from 'fs-extra';
import { normalizePath } from './normalize-path';
import FileFsRef from '../file-fs-ref';
export type GlobOptions = vanillaGlob_.IOptions;
export interface GlobOptions extends vanillaGlob_.IOptions {
includeDirectories?: boolean;
}
const vanillaGlob = promisify(vanillaGlob_);
@@ -15,12 +17,7 @@ export default async function glob(
opts: GlobOptions | string,
mountpoint?: string
): Promise<Record<string, FileFsRef>> {
let options: GlobOptions;
if (typeof opts === 'string') {
options = { cwd: opts };
} else {
options = opts;
}
const options = typeof opts === 'string' ? { cwd: opts } : opts;
if (!options.cwd) {
throw new Error(
@@ -34,13 +31,18 @@ export default async function glob(
const results: Record<string, FileFsRef> = {};
const statCache: Record<string, Stats> = {};
const symlinks: Record<string, boolean | undefined> = {};
options.symlinks = {};
options.statCache = statCache;
options.stat = true;
options.dot = true;
const files = await vanillaGlob(pattern, {
...options,
symlinks,
statCache,
stat: true,
dot: true,
});
const files = await vanillaGlob(pattern, options);
const dirs = new Set<string>();
const dirsWithEntries = new Set<string>();
for (const relativePath of files) {
const fsPath = normalizePath(path.join(options.cwd, relativePath));
@@ -49,12 +51,20 @@ export default async function glob(
stat,
`statCache does not contain value for ${relativePath} (resolved to ${fsPath})`
);
const isSymlink = options.symlinks![fsPath];
if (isSymlink || stat.isFile()) {
const isSymlink = symlinks[fsPath];
if (isSymlink || stat.isFile() || stat.isDirectory()) {
if (isSymlink) {
stat = await lstat(fsPath);
}
// Some bookkeeping to track which directories already have entries within
const dirname = path.dirname(relativePath);
dirsWithEntries.add(dirname);
if (stat.isDirectory()) {
dirs.add(relativePath);
continue;
}
let finalPath = relativePath;
if (mountpoint) {
finalPath = path.join(mountpoint, finalPath);
@@ -64,5 +74,22 @@ export default async function glob(
}
}
// Add empty directory entries
if (options.includeDirectories) {
for (const relativePath of dirs) {
if (dirsWithEntries.has(relativePath)) continue;
let finalPath = relativePath;
if (mountpoint) {
finalPath = path.join(mountpoint, finalPath);
}
const fsPath = normalizePath(path.join(options.cwd, relativePath));
const stat = statCache[fsPath];
results[finalPath] = new FileFsRef({ mode: stat.mode, fsPath });
}
}
return results;
}

View File

@@ -8,6 +8,7 @@ import download, {
downloadFile,
DownloadedFiles,
isSymbolicLink,
isDirectory,
} from './fs/download';
import getWriteableDirectory from './fs/get-writable-directory';
import glob, { GlobOptions } from './fs/glob';
@@ -82,6 +83,7 @@ export {
streamToBuffer,
debug,
isSymbolicLink,
isDirectory,
getLambdaOptionsFromFunction,
scanParentDirs,
getIgnoreFilter,

View File

@@ -0,0 +1,195 @@
import path from 'path';
import fs, { readlink } from 'fs-extra';
import { strict as assert, strictEqual } from 'assert';
import { download, glob, FileBlob } from '../src';
describe('download()', () => {
let warningMessages: string[];
const originalConsoleWarn = console.warn;
beforeEach(() => {
warningMessages = [];
console.warn = m => {
warningMessages.push(m);
};
});
afterEach(() => {
console.warn = originalConsoleWarn;
});
it('should re-create FileFsRef symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 4);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files2 = await download(files, outDir);
assert.equal(Object.keys(files2).length, 4);
const [linkStat, linkDirStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'link-dir')),
fs.lstat(path.join(outDir, 'a.txt')),
]);
assert(linkStat.isSymbolicLink());
assert(linkDirStat.isSymbolicLink());
assert(aStat.isFile());
const [linkDirContents, linkTextContents] = await Promise.all([
readlink(path.join(outDir, 'link-dir')),
readlink(path.join(outDir, 'link.txt')),
]);
strictEqual(linkDirContents, 'dir');
strictEqual(linkTextContents, './a.txt');
});
it('should re-create FileBlob symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = {
'a.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'a text',
}),
'dir/b.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'b text',
}),
'link-dir': new FileBlob({
mode: 41453,
contentType: undefined,
data: 'dir',
}),
'link.txt': new FileBlob({
mode: 41453,
contentType: undefined,
data: 'a.txt',
}),
};
strictEqual(Object.keys(files).length, 4);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files2 = await download(files, outDir);
strictEqual(Object.keys(files2).length, 4);
const [linkStat, linkDirStat, aStat, dirStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'link-dir')),
fs.lstat(path.join(outDir, 'a.txt')),
fs.lstat(path.join(outDir, 'dir')),
]);
assert(linkStat.isSymbolicLink());
assert(linkDirStat.isSymbolicLink());
assert(aStat.isFile());
assert(dirStat.isDirectory());
const [linkDirContents, linkTextContents] = await Promise.all([
readlink(path.join(outDir, 'link-dir')),
readlink(path.join(outDir, 'link.txt')),
]);
strictEqual(linkDirContents, 'dir');
strictEqual(linkTextContents, 'a.txt');
});
it('should download symlinks even with incorrect file', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = {
'dir/file.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'file text',
}),
linkdir: new FileBlob({
mode: 41453,
contentType: undefined,
data: 'dir',
}),
'linkdir/file.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'this file should be discarded',
}),
};
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
await fs.mkdirp(outDir);
await download(files, outDir);
const [dir, file, linkdir] = await Promise.all([
fs.lstat(path.join(outDir, 'dir')),
fs.lstat(path.join(outDir, 'dir/file.txt')),
fs.lstat(path.join(outDir, 'linkdir')),
]);
expect(dir.isFile()).toBe(false);
expect(dir.isSymbolicLink()).toBe(false);
expect(file.isFile()).toBe(true);
expect(file.isSymbolicLink()).toBe(false);
expect(linkdir.isSymbolicLink()).toBe(true);
expect(warningMessages).toEqual([
'Warning: file "linkdir/file.txt" is within a symlinked directory "linkdir" and will be ignored',
]);
});
it('should create empty directory entries', async () => {
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files = {
'empty-dir': new FileBlob({
mode: 16877, // drwxr-xr-x
contentType: undefined,
data: '',
}),
dir: new FileBlob({
mode: 16877,
contentType: undefined,
data: '',
}),
'dir/subdir': new FileBlob({
mode: 16877,
contentType: undefined,
data: '',
}),
'another/subdir': new FileBlob({
mode: 16895, // drwxrwxrwx
contentType: undefined,
data: '',
}),
};
await download(files, outDir);
for (const [p, f] of Object.entries(files)) {
const stat = await fs.lstat(path.join(outDir, p));
expect(stat.isDirectory()).toEqual(true);
if (process.platform !== 'win32') {
// Don't test Windows since it doesn't support the same permissions
expect(stat.mode).toEqual(f.mode);
}
}
});
});

View File

@@ -0,0 +1,65 @@
import fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'os';
import { glob, isDirectory } from '../src';
describe('glob()', () => {
it('should not return entries for empty directories by default', async () => {
const dir = await fs.mkdtemp(join(tmpdir(), 'build-utils-test'));
try {
await Promise.all([
fs.writeFile(join(dir, 'root.txt'), 'file at the root'),
fs.mkdirp(join(dir, 'empty-dir')),
fs
.mkdirp(join(dir, 'dir-with-file'))
.then(() =>
fs.writeFile(join(dir, 'dir-with-file/data.json'), '{"a":"b"}')
),
fs.mkdirp(join(dir, 'another/subdir')),
]);
const files = await glob('**', dir);
const fileNames = Object.keys(files).sort();
expect(fileNames).toHaveLength(2);
expect(fileNames).toEqual(['dir-with-file/data.json', 'root.txt']);
expect(isDirectory(files['dir-with-file/data.json'].mode)).toEqual(false);
expect(isDirectory(files['root.txt'].mode)).toEqual(false);
expect(files['dir-with-file']).toBeUndefined();
expect(files['another/subdir']).toBeUndefined();
expect(files['empty-dir']).toBeUndefined();
} finally {
await fs.remove(dir);
}
});
it('should return entries for empty directories with `includeDirectories: true`', async () => {
const dir = await fs.mkdtemp(join(tmpdir(), 'build-utils-test'));
try {
await Promise.all([
fs.writeFile(join(dir, 'root.txt'), 'file at the root'),
fs.mkdirp(join(dir, 'empty-dir')),
fs
.mkdirp(join(dir, 'dir-with-file'))
.then(() =>
fs.writeFile(join(dir, 'dir-with-file/data.json'), '{"a":"b"}')
),
fs.mkdirp(join(dir, 'another/subdir')),
]);
const files = await glob('**', { cwd: dir, includeDirectories: true });
const fileNames = Object.keys(files).sort();
expect(fileNames).toHaveLength(4);
expect(fileNames).toEqual([
'another/subdir',
'dir-with-file/data.json',
'empty-dir',
'root.txt',
]);
expect(isDirectory(files['another/subdir'].mode)).toEqual(true);
expect(isDirectory(files['empty-dir'].mode)).toEqual(true);
expect(isDirectory(files['dir-with-file/data.json'].mode)).toEqual(false);
expect(isDirectory(files['root.txt'].mode)).toEqual(false);
expect(files['dir-with-file']).toBeUndefined();
} finally {
await fs.remove(dir);
}
});
});

View File

@@ -1,18 +1,15 @@
import ms from 'ms';
import path from 'path';
import fs, { readlink } from 'fs-extra';
import { strict as assert, strictEqual } from 'assert';
import fs from 'fs-extra';
import { strict as assert } from 'assert';
import { getSupportedNodeVersion } from '../src/fs/node-version';
import download from '../src/fs/download';
import {
glob,
getNodeVersion,
getLatestNodeVersion,
getDiscontinuedNodeVersions,
runNpmInstall,
runPackageJsonScript,
scanParentDirs,
FileBlob,
Prerender,
} from '../src';
@@ -49,143 +46,6 @@ afterEach(() => {
console.warn = originalConsoleWarn;
});
it('should re-create FileFsRef symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 4);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files2 = await download(files, outDir);
assert.equal(Object.keys(files2).length, 4);
const [linkStat, linkDirStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'link-dir')),
fs.lstat(path.join(outDir, 'a.txt')),
]);
assert(linkStat.isSymbolicLink());
assert(linkDirStat.isSymbolicLink());
assert(aStat.isFile());
const [linkDirContents, linkTextContents] = await Promise.all([
readlink(path.join(outDir, 'link-dir')),
readlink(path.join(outDir, 'link.txt')),
]);
strictEqual(linkDirContents, 'dir');
strictEqual(linkTextContents, './a.txt');
});
it('should re-create FileBlob symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = {
'a.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'a text',
}),
'dir/b.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'b text',
}),
'link-dir': new FileBlob({
mode: 41453,
contentType: undefined,
data: 'dir',
}),
'link.txt': new FileBlob({
mode: 41453,
contentType: undefined,
data: 'a.txt',
}),
};
strictEqual(Object.keys(files).length, 4);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files2 = await download(files, outDir);
strictEqual(Object.keys(files2).length, 4);
const [linkStat, linkDirStat, aStat, dirStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'link-dir')),
fs.lstat(path.join(outDir, 'a.txt')),
fs.lstat(path.join(outDir, 'dir')),
]);
assert(linkStat.isSymbolicLink());
assert(linkDirStat.isSymbolicLink());
assert(aStat.isFile());
assert(dirStat.isDirectory());
const [linkDirContents, linkTextContents] = await Promise.all([
readlink(path.join(outDir, 'link-dir')),
readlink(path.join(outDir, 'link.txt')),
]);
strictEqual(linkDirContents, 'dir');
strictEqual(linkTextContents, 'a.txt');
});
it('should download symlinks even with incorrect file', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = {
'dir/file.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'file text',
}),
linkdir: new FileBlob({
mode: 41453,
contentType: undefined,
data: 'dir',
}),
'linkdir/file.txt': new FileBlob({
mode: 33188,
contentType: undefined,
data: 'this file should be discarded',
}),
};
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
await fs.mkdirp(outDir);
await download(files, outDir);
const [dir, file, linkdir] = await Promise.all([
fs.lstat(path.join(outDir, 'dir')),
fs.lstat(path.join(outDir, 'dir/file.txt')),
fs.lstat(path.join(outDir, 'linkdir')),
]);
expect(dir.isFile()).toBe(false);
expect(dir.isSymbolicLink()).toBe(false);
expect(file.isFile()).toBe(true);
expect(file.isSymbolicLink()).toBe(false);
expect(linkdir.isSymbolicLink()).toBe(true);
expect(warningMessages).toEqual([
'Warning: file "linkdir/file.txt" is within a symlinked directory "linkdir" and will be ignored',
]);
});
it('should only match supported node versions, otherwise throw an error', async () => {
expect(await getSupportedNodeVersion('14.x', false)).toHaveProperty(
'major',

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.12.0",
"version": "28.13.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -41,16 +41,16 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/go": "workspace:2.2.25",
"@vercel/hydrogen": "workspace:0.0.39",
"@vercel/next": "workspace:3.3.10",
"@vercel/node": "workspace:2.8.7",
"@vercel/python": "workspace:3.1.35",
"@vercel/redwood": "workspace:1.0.46",
"@vercel/remix": "workspace:1.2.0",
"@vercel/ruby": "workspace:1.3.51",
"@vercel/static-build": "workspace:1.1.2"
"@vercel/build-utils": "5.9.0",
"@vercel/go": "2.2.30",
"@vercel/hydrogen": "0.0.44",
"@vercel/next": "3.3.18",
"@vercel/node": "2.8.15",
"@vercel/python": "3.1.40",
"@vercel/redwood": "1.0.51",
"@vercel/remix": "1.2.7",
"@vercel/ruby": "1.3.56",
"@vercel/static-build": "1.2.0"
},
"devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5",
@@ -93,13 +93,13 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "workspace:12.2.27",
"@vercel/error-utils": "workspace:1.0.4",
"@vercel/frameworks": "workspace:1.2.0",
"@vercel/fs-detectors": "workspace:3.7.0",
"@vercel/client": "12.3.2",
"@vercel/error-utils": "1.0.8",
"@vercel/frameworks": "1.2.4",
"@vercel/fs-detectors": "3.7.5",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "workspace:2.1.4",
"@vercel/routing-utils": "2.1.8",
"@zeit/source-map-support": "0.6.2",
"ajv": "6.12.2",
"alpha-sort": "2.0.1",

View File

@@ -1,16 +1,5 @@
import { join } from 'path';
import ms from 'ms';
import fs, { mkdirp } from 'fs-extra';
const {
exec,
fetch,
fixture,
sleep,
testFixture,
testFixtureStdio,
validateResponseHeaders,
} = require('./utils.js');
import { isIP } from 'net';
const { exec, fixture, testFixture, testFixtureStdio } = require('./utils.js');
test('[vercel dev] validate redirects', async () => {
const directory = fixture('invalid-redirects');
@@ -124,260 +113,112 @@ test(
);
test(
'[vercel dev] test cleanUrls serve correct content',
testFixtureStdio('test-clean-urls', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', 'Redirecting to / (308)', {
Location: '/',
});
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
Location: '/about',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
Location: '/sub',
'[vercel dev] Use `@vercel/python` with Flask requirements.txt',
testFixtureStdio('python-flask', async (testPath: any) => {
const name = 'Alice';
const year = new Date().getFullYear();
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/headers`, (body: any, res: any) => {
// @ts-ignore
const { host } = new URL(res.url);
expect(body).toBe(host);
});
})
);
test(
'[vercel dev] Use custom runtime from the "functions" property',
testFixtureStdio('custom-runtime', async (testPath: any) => {
await testPath(200, `/api/user`, /Hello, from Bash!/m);
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
})
);
test(
'[vercel dev] Should work with nested `tsconfig.json` files',
testFixtureStdio('nested-tsconfig', async (testPath: any) => {
await testPath(200, `/`, /Nested tsconfig.json test page/);
await testPath(200, `/api`, 'Nested `tsconfig.json` API endpoint');
})
);
test(
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
testFixtureStdio('force-module-commonjs', async (testPath: any) => {
await testPath(200, `/`, /Force &quot;module: commonjs&quot; test page/);
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another (308)',
{ Location: '/sub/another' }
200,
`/api`,
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
);
await testPath(
200,
`/api/ts`,
'Force "module: commonjs" TypeScript API endpoint'
);
})
);
test(
'[vercel dev] test cleanUrls serve correct content when using `outputDirectory`',
testFixtureStdio(
'test-clean-urls-with-output-directory',
async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', 'Redirecting to / (308)', {
Location: '/',
});
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
Location: '/about',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
Location: '/sub',
});
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another (308)',
{ Location: '/sub/another' }
);
}
)
);
test(
'[vercel dev] should serve custom 404 when `cleanUrls: true`',
testFixtureStdio('test-clean-urls-custom-404', async (testPath: any) => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about', 'The about page');
await testPath(200, '/contact/me', 'Contact Me Subdirectory');
await testPath(404, '/nothing', 'Custom 404 Page');
await testPath(404, '/nothing/', 'Custom 404 Page');
'[vercel dev] should prioritize index.html over other file named index.*',
testFixtureStdio('index-html-priority', async (testPath: any) => {
await testPath(200, '/', 'This is index.html');
await testPath(200, '/index.css', 'This is index.css');
})
);
test(
'[vercel dev] test cleanUrls and trailingSlash serve correct content',
testFixtureStdio('test-clean-urls-trailing-slash', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about/', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/another/', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
//TODO: fix this test so that location is `/` instead of `//`
//await testPath(308, '/index.html', 'Redirecting to / (308)', { Location: '/' });
await testPath(308, '/about.html', 'Redirecting to /about/ (308)', {
Location: '/about/',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub/ (308)', {
Location: '/sub/',
});
'[vercel dev] Should support `*.go` API serverless functions',
testFixtureStdio('go', async (testPath: any) => {
await testPath(200, `/api`, 'This is the index page');
await testPath(200, `/api/index`, 'This is the index page');
await testPath(200, `/api/index.go`, 'This is the index page');
await testPath(200, `/api/another`, 'This is another page');
await testPath(200, '/api/another.go', 'This is another page');
await testPath(200, `/api/foo`, 'Req Path: /api/foo');
await testPath(200, `/api/bar`, 'Req Path: /api/bar');
})
);
test(
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
testFixtureStdio('node-ts-node-target', async (testPath: any) => {
await testPath(200, `/api/subclass`, '{"ok":true}');
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another/ (308)',
{
Location: '/sub/another/',
}
200,
`/api/array`,
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
);
})
);
test(
'[vercel dev] test cors headers work with OPTIONS',
testFixtureStdio('test-cors-routes', async (testPath: any) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'Content-Type, Authorization, Accept, Content-Length, Origin, User-Agent',
'Access-Control-Allow-Methods':
'GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE',
};
await testPath(200, '/', 'status api', headers, { method: 'GET' });
await testPath(200, '/', 'status api', headers, { method: 'POST' });
await testPath(200, '/api/status.js', 'status api', headers, {
method: 'GET',
});
await testPath(200, '/api/status.js', 'status api', headers, {
method: 'POST',
});
await testPath(204, '/', '', headers, { method: 'OPTIONS' });
await testPath(204, '/api/status.js', '', headers, { method: 'OPTIONS' });
})
);
await testPath(200, `/api/dump`, (body: any, res: any, isDev: any) => {
// @ts-ignore
const { host } = new URL(res.url);
const { env, headers } = JSON.parse(body);
test(
'[vercel dev] test trailingSlash true serve correct content',
testFixtureStdio('test-trailing-slash', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
Location: '/about.html',
});
await testPath(308, '/style.css/', 'Redirecting to /style.css (308)', {
Location: '/style.css',
});
await testPath(308, '/sub', 'Redirecting to /sub/ (308)', {
Location: '/sub/',
});
})
);
// Test that the API endpoint receives the Vercel proxy request headers
expect(headers['x-forwarded-host']).toBe(host);
expect(headers['x-vercel-deployment-url']).toBe(host);
expect(isIP(headers['x-real-ip'])).toBeTruthy();
expect(isIP(headers['x-forwarded-for'])).toBeTruthy();
expect(isIP(headers['x-vercel-forwarded-for'])).toBeTruthy();
test(
'[vercel dev] should serve custom 404 when `trailingSlash: true`',
testFixtureStdio('test-trailing-slash-custom-404', async (testPath: any) => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about.html', 'The about page');
await testPath(200, '/contact/', 'Contact Subdirectory');
await testPath(404, '/nothing/', 'Custom 404 Page');
})
);
test(
'[vercel dev] test trailingSlash false serve correct content',
testFixtureStdio('test-trailing-slash-false', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
Location: '/about.html',
});
await testPath(308, '/sub/', 'Redirecting to /sub (308)', {
Location: '/sub',
});
await testPath(
308,
'/sub/another.html/',
'Redirecting to /sub/another.html (308)',
{
Location: '/sub/another.html',
// Test that the API endpoint has the Vercel platform env vars defined.
expect(env.NOW_REGION).toMatch(/^[a-z]{3}\d$/);
if (isDev) {
// Only dev is tested because in production these are opt-in.
expect(env.VERCEL_URL).toBe(host);
expect(env.VERCEL_REGION).toBe('dev1');
}
);
});
})
);
test(
'[vercel dev] throw when invalid builder routes detected',
testFixtureStdio(
'invalid-builder-routes',
async (testPath: any) => {
await testPath(
500,
'/',
/Route at index 0 has invalid `src` regular expression/m
);
},
{ skipDeploy: true }
)
);
test(
'[vercel dev] support legacy `@now` scope runtimes',
testFixtureStdio('legacy-now-runtime', async (testPath: any) => {
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
})
);
test(
'[vercel dev] 00-list-directory',
testFixtureStdio(
'00-list-directory',
async (testPath: any) => {
await testPath(200, '/', /Files within/m);
await testPath(200, '/', /test[0-3]\.txt/m);
await testPath(200, '/', /\.well-known/m);
await testPath(200, '/.well-known/keybase.txt', 'proof goes here');
},
{ projectSettings: { directoryListing: true } }
)
);
test(
'[vercel dev] 01-node',
testFixtureStdio('01-node', async (testPath: any) => {
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
})
);
test(
'[vercel dev] add a `api/fn.ts` when `api` does not exist at startup`',
testFixtureStdio('no-api', async (_testPath: any, port: any) => {
const directory = fixture('no-api');
const apiDir = join(directory, 'api');
try {
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
expect(response.status).toBe(404);
}
const fileContents = `
export const config = {
runtime: 'edge'
}
export default async function edge(request, event) {
return new Response('from new file');
}
`;
await mkdirp(apiDir);
await fs.writeFile(join(apiDir, 'new-file.js'), fileContents);
// Wait until file events have been processed
await sleep(ms('1s'));
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe('from new file');
}
} finally {
await fs.remove(apiDir);
}
'[vercel dev] Do not fail if `src` is missing',
testFixtureStdio('missing-src-property', async (testPath: any) => {
await testPath(200, '/', /hello:index.txt/m);
await testPath(404, '/i-do-not-exist');
})
);

View File

@@ -1,196 +1,15 @@
import ms from 'ms';
import fs from 'fs-extra';
import { isIP } from 'net';
import { join } from 'path';
import { Response } from 'node-fetch';
const {
fetch,
sleep,
fixture,
testFixture,
testFixtureStdio,
validateResponseHeaders,
} = require('./utils.js');
test(
'[vercel dev] temporary directory listing',
testFixtureStdio(
'temporary-directory-listing',
async (_testPath: any, port: any) => {
const directory = fixture('temporary-directory-listing');
await fs.unlink(join(directory, 'index.txt')).catch(() => null);
await sleep(ms('20s'));
const firstResponse = await fetch(`http://localhost:${port}`);
validateResponseHeaders(firstResponse);
const body = await firstResponse.text();
console.log(body);
expect(firstResponse.status).toBe(404);
await fs.writeFile(join(directory, 'index.txt'), 'hello');
for (let i = 0; i < 20; i++) {
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
if (response.status === 200) {
const body = await response.text();
expect(body).toBe('hello');
}
await sleep(ms('1s'));
}
},
{ skipDeploy: true }
)
);
test('[vercel dev] add a `package.json` to trigger `@vercel/static-build`', async () => {
const directory = fixture('trigger-static-build');
await fs.unlink(join(directory, 'package.json')).catch(() => null);
await fs.unlink(join(directory, 'public', 'index.txt')).catch(() => null);
await fs.rmdir(join(directory, 'public')).catch(() => null);
const tester = testFixtureStdio(
'trigger-static-build',
async (_testPath: any, port: any) => {
{
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe('hello:index.txt');
}
const rnd = Math.random().toString();
const pkg = {
private: true,
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
};
await fs.writeFile(join(directory, 'package.json'), JSON.stringify(pkg));
// Wait until file events have been processed
await sleep(ms('2s'));
{
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe(rnd);
}
},
{ skipDeploy: true }
);
await tester();
});
test('[vercel dev] no build matches warning', async () => {
const directory = fixture('no-build-matches');
const { dev } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
// start `vercel dev` detached in child_process
dev.unref();
dev.stderr.setEncoding('utf8');
await new Promise<void>(resolve => {
dev.stderr.on('data', (str: string) => {
if (str.includes('did not match any source files')) {
resolve();
}
});
});
} finally {
await dev.kill();
}
});
test(
'[vercel dev] do not recursivly check the path',
testFixtureStdio('handle-filesystem-missing', async (testPath: any) => {
await testPath(200, '/', /hello/m);
await testPath(404, '/favicon.txt');
})
);
test('[vercel dev] render warning for empty cwd dir', async () => {
const directory = fixture('empty');
const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
dev.unref();
// Monitor `stderr` for the warning
dev.stderr.setEncoding('utf8');
const msg = 'There are no files inside your deployment.';
await new Promise<void>(resolve => {
dev.stderr.on('data', (str: string) => {
if (str.includes(msg)) {
resolve();
}
});
});
// Issue a request to ensure a 404 response
await sleep(ms('3s'));
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
expect(response.status).toBe(404);
} finally {
await dev.kill();
}
});
test('[vercel dev] do not rebuild for changes in the output directory', async () => {
const directory = fixture('output-is-source');
const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
dev.unref();
let stderr: any = [];
const start = Date.now();
dev.stderr.on('data', (str: any) => stderr.push(str));
while (stderr.join('').includes('Ready') === false) {
await sleep(ms('3s'));
if (Date.now() - start > ms('30s')) {
console.log('stderr:', stderr.join(''));
break;
}
}
const resp1 = await fetch(`http://localhost:${port}`);
const text1 = await resp1.text();
expect(text1.trim()).toBe('hello first');
await fs.writeFile(join(directory, 'public', 'index.html'), 'hello second');
await sleep(ms('3s'));
const resp2 = await fetch(`http://localhost:${port}`);
const text2 = await resp2.text();
expect(text2.trim()).toBe('hello second');
} finally {
await dev.kill();
}
});
test(
'[vercel dev] 25-nextjs-src-dir',
testFixtureStdio('25-nextjs-src-dir', async (testPath: any) => {
@@ -324,117 +143,6 @@ test(
})
);
test(
'[vercel dev] Use `@vercel/python` with Flask requirements.txt',
testFixtureStdio('python-flask', async (testPath: any) => {
const name = 'Alice';
const year = new Date().getFullYear();
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
await testPath(200, `/api/headers`, (body: any, res: any) => {
// @ts-ignore
const { host } = new URL(res.url);
expect(body).toBe(host);
});
})
);
test(
'[vercel dev] Use custom runtime from the "functions" property',
testFixtureStdio('custom-runtime', async (testPath: any) => {
await testPath(200, `/api/user`, /Hello, from Bash!/m);
await testPath(200, `/api/user.sh`, /Hello, from Bash!/m);
})
);
test(
'[vercel dev] Should work with nested `tsconfig.json` files',
testFixtureStdio('nested-tsconfig', async (testPath: any) => {
await testPath(200, `/`, /Nested tsconfig.json test page/);
await testPath(200, `/api`, 'Nested `tsconfig.json` API endpoint');
})
);
test(
'[vercel dev] Should force `tsc` option "module: commonjs" for `startDevServer()`',
testFixtureStdio('force-module-commonjs', async (testPath: any) => {
await testPath(200, `/`, /Force &quot;module: commonjs&quot; test page/);
await testPath(
200,
`/api`,
'Force "module: commonjs" JavaScript with ES Modules API endpoint'
);
await testPath(
200,
`/api/ts`,
'Force "module: commonjs" TypeScript API endpoint'
);
})
);
test(
'[vercel dev] should prioritize index.html over other file named index.*',
testFixtureStdio('index-html-priority', async (testPath: any) => {
await testPath(200, '/', 'This is index.html');
await testPath(200, '/index.css', 'This is index.css');
})
);
test(
'[vercel dev] Should support `*.go` API serverless functions',
testFixtureStdio('go', async (testPath: any) => {
await testPath(200, `/api`, 'This is the index page');
await testPath(200, `/api/index`, 'This is the index page');
await testPath(200, `/api/index.go`, 'This is the index page');
await testPath(200, `/api/another`, 'This is another page');
await testPath(200, '/api/another.go', 'This is another page');
await testPath(200, `/api/foo`, 'Req Path: /api/foo');
await testPath(200, `/api/bar`, 'Req Path: /api/bar');
})
);
test(
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
testFixtureStdio('node-ts-node-target', async (testPath: any) => {
await testPath(200, `/api/subclass`, '{"ok":true}');
await testPath(
200,
`/api/array`,
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
);
await testPath(200, `/api/dump`, (body: any, res: any, isDev: any) => {
// @ts-ignore
const { host } = new URL(res.url);
const { env, headers } = JSON.parse(body);
// Test that the API endpoint receives the Vercel proxy request headers
expect(headers['x-forwarded-host']).toBe(host);
expect(headers['x-vercel-deployment-url']).toBe(host);
expect(isIP(headers['x-real-ip'])).toBeTruthy();
expect(isIP(headers['x-forwarded-for'])).toBeTruthy();
expect(isIP(headers['x-vercel-forwarded-for'])).toBeTruthy();
// Test that the API endpoint has the Vercel platform env vars defined.
expect(env.NOW_REGION).toMatch(/^[a-z]{3}\d$/);
if (isDev) {
// Only dev is tested because in production these are opt-in.
expect(env.VERCEL_URL).toBe(host);
expect(env.VERCEL_REGION).toBe('dev1');
}
});
})
);
test(
'[vercel dev] Do not fail if `src` is missing',
testFixtureStdio('missing-src-property', async (testPath: any) => {
await testPath(200, '/', /hello:index.txt/m);
await testPath(404, '/i-do-not-exist');
})
);
test(
'[vercel dev] Middleware that returns a 200 response',
testFixtureStdio('middleware-response', async (testPath: any) => {

View File

@@ -0,0 +1,449 @@
import { join } from 'path';
import ms from 'ms';
import fs, { mkdirp } from 'fs-extra';
const {
fetch,
fixture,
sleep,
testFixture,
testFixtureStdio,
validateResponseHeaders,
} = require('./utils.js');
test(
'[vercel dev] temporary directory listing',
testFixtureStdio(
'temporary-directory-listing',
async (_testPath: any, port: any) => {
const directory = fixture('temporary-directory-listing');
await fs.unlink(join(directory, 'index.txt')).catch(() => null);
await sleep(ms('20s'));
const firstResponse = await fetch(`http://localhost:${port}`);
validateResponseHeaders(firstResponse);
const body = await firstResponse.text();
console.log(body);
expect(firstResponse.status).toBe(404);
await fs.writeFile(join(directory, 'index.txt'), 'hello');
for (let i = 0; i < 20; i++) {
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
if (response.status === 200) {
const body = await response.text();
expect(body).toBe('hello');
}
await sleep(ms('1s'));
}
},
{ skipDeploy: true }
)
);
test('[vercel dev] add a `package.json` to trigger `@vercel/static-build`', async () => {
const directory = fixture('trigger-static-build');
await fs.unlink(join(directory, 'package.json')).catch(() => null);
await fs.unlink(join(directory, 'public', 'index.txt')).catch(() => null);
await fs.rmdir(join(directory, 'public')).catch(() => null);
const tester = testFixtureStdio(
'trigger-static-build',
async (_testPath: any, port: any) => {
{
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe('hello:index.txt');
}
const rnd = Math.random().toString();
const pkg = {
private: true,
scripts: { build: `mkdir -p public && echo ${rnd} > public/index.txt` },
};
await fs.writeFile(join(directory, 'package.json'), JSON.stringify(pkg));
// Wait until file events have been processed
await sleep(ms('2s'));
{
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe(rnd);
}
},
{ skipDeploy: true }
);
await tester();
});
test('[vercel dev] no build matches warning', async () => {
const directory = fixture('no-build-matches');
const { dev } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
// start `vercel dev` detached in child_process
dev.unref();
dev.stderr.setEncoding('utf8');
await new Promise<void>(resolve => {
dev.stderr.on('data', (str: string) => {
if (str.includes('did not match any source files')) {
resolve();
}
});
});
} finally {
await dev.kill();
}
});
test(
'[vercel dev] do not recursivly check the path',
testFixtureStdio('handle-filesystem-missing', async (testPath: any) => {
await testPath(200, '/', /hello/m);
await testPath(404, '/favicon.txt');
})
);
test('[vercel dev] render warning for empty cwd dir', async () => {
const directory = fixture('empty');
const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
dev.unref();
// Monitor `stderr` for the warning
dev.stderr.setEncoding('utf8');
const msg = 'There are no files inside your deployment.';
await new Promise<void>(resolve => {
dev.stderr.on('data', (str: string) => {
if (str.includes(msg)) {
resolve();
}
});
});
// Issue a request to ensure a 404 response
await sleep(ms('3s'));
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(response);
expect(response.status).toBe(404);
} finally {
await dev.kill();
}
});
test('[vercel dev] do not rebuild for changes in the output directory', async () => {
const directory = fixture('output-is-source');
const { dev, port } = await testFixture(directory, {
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
dev.unref();
let stderr: any = [];
const start = Date.now();
dev.stderr.on('data', (str: any) => stderr.push(str));
while (stderr.join('').includes('Ready') === false) {
await sleep(ms('3s'));
if (Date.now() - start > ms('30s')) {
console.log('stderr:', stderr.join(''));
break;
}
}
const resp1 = await fetch(`http://localhost:${port}`);
const text1 = await resp1.text();
expect(text1.trim()).toBe('hello first');
await fs.writeFile(join(directory, 'public', 'index.html'), 'hello second');
await sleep(ms('3s'));
const resp2 = await fetch(`http://localhost:${port}`);
const text2 = await resp2.text();
expect(text2.trim()).toBe('hello second');
} finally {
await dev.kill();
}
});
test(
'[vercel dev] test cleanUrls serve correct content',
testFixtureStdio('test-clean-urls', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', 'Redirecting to / (308)', {
Location: '/',
});
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
Location: '/about',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
Location: '/sub',
});
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another (308)',
{ Location: '/sub/another' }
);
})
);
test(
'[vercel dev] test cleanUrls serve correct content when using `outputDirectory`',
testFixtureStdio(
'test-clean-urls-with-output-directory',
async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', 'Redirecting to / (308)', {
Location: '/',
});
await testPath(308, '/about.html', 'Redirecting to /about (308)', {
Location: '/about',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub (308)', {
Location: '/sub',
});
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another (308)',
{ Location: '/sub/another' }
);
}
)
);
test(
'[vercel dev] should serve custom 404 when `cleanUrls: true`',
testFixtureStdio('test-clean-urls-custom-404', async (testPath: any) => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about', 'The about page');
await testPath(200, '/contact/me', 'Contact Me Subdirectory');
await testPath(404, '/nothing', 'Custom 404 Page');
await testPath(404, '/nothing/', 'Custom 404 Page');
})
);
test(
'[vercel dev] test cleanUrls and trailingSlash serve correct content',
testFixtureStdio('test-clean-urls-trailing-slash', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/about/', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/another/', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
//TODO: fix this test so that location is `/` instead of `//`
//await testPath(308, '/index.html', 'Redirecting to / (308)', { Location: '/' });
await testPath(308, '/about.html', 'Redirecting to /about/ (308)', {
Location: '/about/',
});
await testPath(308, '/sub/index.html', 'Redirecting to /sub/ (308)', {
Location: '/sub/',
});
await testPath(
308,
'/sub/another.html',
'Redirecting to /sub/another/ (308)',
{
Location: '/sub/another/',
}
);
})
);
test(
'[vercel dev] test cors headers work with OPTIONS',
testFixtureStdio('test-cors-routes', async (testPath: any) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'Content-Type, Authorization, Accept, Content-Length, Origin, User-Agent',
'Access-Control-Allow-Methods':
'GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE',
};
await testPath(200, '/', 'status api', headers, { method: 'GET' });
await testPath(200, '/', 'status api', headers, { method: 'POST' });
await testPath(200, '/api/status.js', 'status api', headers, {
method: 'GET',
});
await testPath(200, '/api/status.js', 'status api', headers, {
method: 'POST',
});
await testPath(204, '/', '', headers, { method: 'OPTIONS' });
await testPath(204, '/api/status.js', '', headers, { method: 'OPTIONS' });
})
);
test(
'[vercel dev] test trailingSlash true serve correct content',
testFixtureStdio('test-trailing-slash', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
Location: '/about.html',
});
await testPath(308, '/style.css/', 'Redirecting to /style.css (308)', {
Location: '/style.css',
});
await testPath(308, '/sub', 'Redirecting to /sub/ (308)', {
Location: '/sub/',
});
})
);
test(
'[vercel dev] should serve custom 404 when `trailingSlash: true`',
testFixtureStdio('test-trailing-slash-custom-404', async (testPath: any) => {
await testPath(200, '/', 'This is the home page');
await testPath(200, '/about.html', 'The about page');
await testPath(200, '/contact/', 'Contact Subdirectory');
await testPath(404, '/nothing/', 'Custom 404 Page');
})
);
test(
'[vercel dev] test trailingSlash false serve correct content',
testFixtureStdio('test-trailing-slash-false', async (testPath: any) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/about.html/', 'Redirecting to /about.html (308)', {
Location: '/about.html',
});
await testPath(308, '/sub/', 'Redirecting to /sub (308)', {
Location: '/sub',
});
await testPath(
308,
'/sub/another.html/',
'Redirecting to /sub/another.html (308)',
{
Location: '/sub/another.html',
}
);
})
);
test(
'[vercel dev] throw when invalid builder routes detected',
testFixtureStdio(
'invalid-builder-routes',
async (testPath: any) => {
await testPath(
500,
'/',
/Route at index 0 has invalid `src` regular expression/m
);
},
{ skipDeploy: true }
)
);
test(
'[vercel dev] support legacy `@now` scope runtimes',
testFixtureStdio('legacy-now-runtime', async (testPath: any) => {
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
})
);
test(
'[vercel dev] 00-list-directory',
testFixtureStdio(
'00-list-directory',
async (testPath: any) => {
await testPath(200, '/', /Files within/m);
await testPath(200, '/', /test[0-3]\.txt/m);
await testPath(200, '/', /\.well-known/m);
await testPath(200, '/.well-known/keybase.txt', 'proof goes here');
},
{ projectSettings: { directoryListing: true } }
)
);
test(
'[vercel dev] 01-node',
testFixtureStdio('01-node', async (testPath: any) => {
await testPath(200, '/', /A simple deployment with the Vercel API!/m);
})
);
test(
'[vercel dev] add a `api/fn.ts` when `api` does not exist at startup`',
testFixtureStdio('no-api', async (_testPath: any, port: any) => {
const directory = fixture('no-api');
const apiDir = join(directory, 'api');
try {
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
expect(response.status).toBe(404);
}
const fileContents = `
export const config = {
runtime: 'edge'
}
export default async function edge(request, event) {
return new Response('from new file');
}
`;
await mkdirp(apiDir);
await fs.writeFile(join(apiDir, 'new-file.js'), fileContents);
// Wait until file events have been processed
await sleep(ms('1s'));
{
const response = await fetch(`http://localhost:${port}/api/new-file`);
validateResponseHeaders(response);
const body = await response.text();
expect(body.trim()).toBe('from new file');
}
} finally {
await fs.remove(apiDir);
}
})
);

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.2.27",
"version": "12.3.2",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -23,7 +23,7 @@
"node": ">= 14"
},
"devDependencies": {
"@types/async-retry": "1.4.1",
"@types/async-retry": "1.4.5",
"@types/fs-extra": "7.0.0",
"@types/jest": "27.4.1",
"@types/minimatch": "3.0.5",
@@ -43,8 +43,8 @@
]
},
"dependencies": {
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/routing-utils": "workspace:2.1.4",
"@vercel/build-utils": "5.9.0",
"@vercel/routing-utils": "2.1.8",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,6 +1,6 @@
import sleep from 'sleep-promise';
import ms from 'ms';
import { fetch, getApiDeploymentsUrl } from './utils';
import { getPollingDelay } from './utils/get-polling-delay';
import {
isDone,
isReady,
@@ -47,6 +47,7 @@ export async function* checkDeploymentStatus(
// Build polling
debug('Waiting for builds and the deployment to complete...');
const finishedEvents = new Set();
const startTime = Date.now();
while (true) {
// Deployment polling
@@ -155,6 +156,8 @@ export async function* checkDeploymentStatus(
};
}
await sleep(ms('1.5s'));
const elapsed = Date.now() - startTime;
const duration = getPollingDelay(elapsed);
await sleep(duration);
}
}

View File

@@ -73,7 +73,7 @@ export default function buildCreateDeployment() {
debug(`Provided 'path' is a single file`);
}
let { fileList } = await buildFileTree(path, clientOptions, debug);
const { fileList } = await buildFileTree(path, clientOptions, debug);
// This is a useful warning because it prevents people
// from getting confused about a deployment that renders 404.

View File

@@ -1,4 +1,4 @@
import { DeploymentFile } from './utils/hashes';
import { FilesMap } from './utils/hashes';
import { generateQueryString } from './utils/query-string';
import { isReady, isAliasAssigned } from './utils/ready-state';
import { checkDeploymentStatus } from './check-deployment-status';
@@ -16,7 +16,7 @@ import {
} from './types';
async function* postDeployment(
files: Map<string, DeploymentFile>,
files: FilesMap,
clientOptions: VercelClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{
@@ -90,7 +90,7 @@ async function* postDeployment(
}
function getDefaultName(
files: Map<string, DeploymentFile>,
files: FilesMap,
clientOptions: VercelClientOptions
): string {
const debug = createDebug(clientOptions.debug);
@@ -109,7 +109,7 @@ function getDefaultName(
}
export async function* deploy(
files: Map<string, DeploymentFile>,
files: FilesMap,
clientOptions: VercelClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{ type: string; payload: any }> {

View File

@@ -5,7 +5,7 @@ import { EventEmitter } from 'events';
import retry from 'async-retry';
import { Sema } from 'async-sema';
import { DeploymentFile } from './utils/hashes';
import { DeploymentFile, FilesMap } from './utils/hashes';
import { fetch, API_FILES, createDebug } from './utils';
import { DeploymentError } from './errors';
import { deploy } from './deploy';
@@ -29,7 +29,7 @@ const isClientNetworkError = (err: Error) => {
};
export async function* upload(
files: Map<string, DeploymentFile>,
files: FilesMap,
clientOptions: VercelClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<any> {
@@ -98,6 +98,10 @@ export async function* upload(
await semaphore.acquire();
const { data } = file;
if (typeof data === 'undefined') {
// Directories don't need to be uploaded
return;
}
uploadProgress.bytesUploaded = 0;

View File

@@ -0,0 +1,14 @@
import ms from 'ms';
export function getPollingDelay(elapsed: number): number {
if (elapsed <= ms('15s')) {
return ms('1s');
}
if (elapsed <= ms('1m')) {
return ms('5s');
}
if (elapsed <= ms('5m')) {
return ms('15s');
}
return ms('30s');
}

View File

@@ -4,10 +4,12 @@ import { Sema } from 'async-sema';
export interface DeploymentFile {
names: string[];
data: Buffer;
data?: Buffer;
mode: number;
}
export type FilesMap = Map<string | undefined, DeploymentFile>;
/**
* Computes a hash for the given buf.
*
@@ -23,14 +25,12 @@ export function hash(buf: Buffer): string {
* @param map with hashed files
* @return {object}
*/
export const mapToObject = (
map: Map<string, DeploymentFile>
): { [key: string]: DeploymentFile } => {
export const mapToObject = (map: FilesMap): Record<string, DeploymentFile> => {
const obj: { [key: string]: DeploymentFile } = {};
for (const [key, value] of map) {
if (typeof key === 'undefined') continue;
obj[key] = value;
}
return obj;
};
@@ -43,8 +43,8 @@ export const mapToObject = (
*/
export async function hashes(
files: string[],
map = new Map<string, DeploymentFile>()
): Promise<Map<string, DeploymentFile>> {
map = new Map<string | undefined, DeploymentFile>()
): Promise<FilesMap> {
const semaphore = new Sema(100);
await Promise.all(
@@ -54,15 +54,21 @@ export async function hashes(
const stat = await fs.lstat(name);
const mode = stat.mode;
let data: Buffer | null = null;
if (stat.isSymbolicLink()) {
const link = await fs.readlink(name);
data = Buffer.from(link, 'utf8');
} else {
data = await fs.readFile(name);
let data: Buffer | undefined;
const isDirectory = stat.isDirectory();
let h: string | undefined;
if (!isDirectory) {
if (stat.isSymbolicLink()) {
const link = await fs.readlink(name);
data = Buffer.from(link, 'utf8');
} else {
data = await fs.readFile(name);
}
h = hash(data);
}
const h = hash(data);
const entry = map.get(h);
if (entry) {

View File

@@ -1,4 +1,4 @@
import { DeploymentFile } from './hashes';
import { FilesMap } from './hashes';
import { FetchOptions } from '@zeit/fetch';
import { nodeFetch, zeitFetch } from './fetch';
import { join, sep, relative } from 'path';
@@ -256,15 +256,15 @@ export const fetch = async (
export interface PreparedFile {
file: string;
sha: string;
size: number;
sha?: string;
size?: number;
mode: number;
}
const isWin = process.platform.includes('win');
export const prepareFiles = (
files: Map<string, DeploymentFile>,
files: FilesMap,
clientOptions: VercelClientOptions
): PreparedFile[] => {
const preparedFiles: PreparedFile[] = [];
@@ -286,9 +286,9 @@ export const prepareFiles = (
preparedFiles.push({
file: isWin ? fileName.replace(/\\/g, '/') : fileName,
size: file.data.byteLength || file.data.length,
size: file.data?.byteLength || file.data?.length,
mode: file.mode,
sha,
sha: sha || undefined,
});
}
}

View File

@@ -87,6 +87,10 @@ export default function readdir(
if (stats.isDirectory()) {
readdir(filePath, ignores)
.then(function (res) {
if (res.length === 0) {
// Empty directories get returned
list.push(filePath);
}
list = list.concat(res);
pending -= 1;
if (!pending) {

View File

@@ -0,0 +1,41 @@
import { getPollingDelay } from '../src/utils/get-polling-delay';
describe('getPollingDelay()', () => {
it('should return 1 second', async () => {
expect(getPollingDelay(0)).toBe(1000);
expect(getPollingDelay(1000)).toBe(1000);
expect(getPollingDelay(3000)).toBe(1000);
expect(getPollingDelay(5000)).toBe(1000);
expect(getPollingDelay(8000)).toBe(1000);
expect(getPollingDelay(9000)).toBe(1000);
expect(getPollingDelay(10000)).toBe(1000);
expect(getPollingDelay(13000)).toBe(1000);
expect(getPollingDelay(15000)).toBe(1000);
});
it('should return 5 second', async () => {
expect(getPollingDelay(15001)).toBe(5000);
expect(getPollingDelay(16000)).toBe(5000);
expect(getPollingDelay(23000)).toBe(5000);
expect(getPollingDelay(36000)).toBe(5000);
expect(getPollingDelay(59000)).toBe(5000);
expect(getPollingDelay(60000)).toBe(5000);
});
it('should return 15 second', async () => {
expect(getPollingDelay(60001)).toBe(15000);
expect(getPollingDelay(80000)).toBe(15000);
expect(getPollingDelay(100000)).toBe(15000);
expect(getPollingDelay(200000)).toBe(15000);
expect(getPollingDelay(250000)).toBe(15000);
expect(getPollingDelay(300000)).toBe(15000);
});
it('should return 30 second', async () => {
expect(getPollingDelay(300001)).toBe(30000);
expect(getPollingDelay(400000)).toBe(30000);
expect(getPollingDelay(1400000)).toBe(30000);
expect(getPollingDelay(9400000)).toBe(30000);
expect(getPollingDelay(99400000)).toBe(30000);
});
});

View File

@@ -24,7 +24,11 @@ describe('buildFileTree()', () => {
noop
);
const expectedFileList = toAbsolutePaths(cwd, ['.nowignore', 'index.txt']);
const expectedFileList = toAbsolutePaths(cwd, [
'.nowignore',
'folder',
'index.txt',
]);
expect(normalizeWindowsPaths(expectedFileList).sort()).toEqual(
normalizeWindowsPaths(fileList).sort()
);
@@ -41,9 +45,14 @@ describe('buildFileTree()', () => {
it('should include symlinked files and directories', async () => {
const cwd = fixture('symlinks');
// Also add an empty directory to make sure it's included
await fs.mkdirp(join(cwd, 'empty'));
const { fileList } = await buildFileTree(cwd, { isDirectory: true }, noop);
const expectedFileList = toAbsolutePaths(cwd, [
'empty',
'folder-link',
'folder/text.txt',
'index.txt',

View File

@@ -12,7 +12,8 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"target": "ES2020"
"target": "ES2020",
"skipLibCheck": true
},
"include": ["./src"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/edge",
"version": "0.2.2",
"version": "0.2.6",
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/error-utils",
"version": "1.0.4",
"version": "1.0.8",
"description": "A collection of error utilities for vercel/vercel",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "1.2.0",
"version": "1.2.4",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [
@@ -21,7 +21,7 @@
"@types/js-yaml": "3.12.1",
"@types/node": "14.18.33",
"@types/node-fetch": "2.5.8",
"@vercel/routing-utils": "workspace:2.1.4",
"@vercel/routing-utils": "2.1.8",
"ajv": "6.12.2",
"typescript": "4.3.4"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "3.7.0",
"version": "3.7.5",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -19,9 +19,9 @@
"test-unit": "pnpm test"
},
"dependencies": {
"@vercel/error-utils": "workspace:1.0.4",
"@vercel/frameworks": "workspace:1.2.0",
"@vercel/routing-utils": "workspace:2.1.4",
"@vercel/error-utils": "1.0.8",
"@vercel/frameworks": "1.2.4",
"@vercel/routing-utils": "2.1.8",
"glob": "8.0.3",
"js-yaml": "4.1.0",
"json5": "2.2.2",
@@ -35,7 +35,7 @@
"@types/minimatch": "3.0.5",
"@types/node": "14.18.33",
"@types/semver": "7.3.10",
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/build-utils": "5.9.0",
"typescript": "4.3.4"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/gatsby-plugin-vercel-analytics",
"version": "1.0.2",
"version": "1.0.6",
"description": "Track Core Web Vitals in Gatsby projects with Vercel Analytics.",
"main": "index.js",
"files": [

View File

@@ -0,0 +1,4 @@
gatsby-node.*
!gatsby-node.ts
dist

View File

@@ -0,0 +1,28 @@
import path from 'path';
import type { GatsbyNode } from 'gatsby';
// this gets built separately, so import from "dist" instead of "src"
import { generateVercelBuildOutputAPI3Output } from './dist';
export const pluginOptionsSchema: GatsbyNode['pluginOptionsSchema'] = ({
Joi,
}) => {
return Joi.object({
exportPath: Joi.string().optional(),
});
};
export const onPostBuild: GatsbyNode['onPostBuild'] = async (
{ store },
pluginOptions
) => {
// validated by `pluginOptionSchema`
const exportPath = (pluginOptions?.exportPath ??
path.join('.vercel', 'output', 'config.json')) as string;
await generateVercelBuildOutputAPI3Output({
exportPath,
gatsbyStoreState: store.getState(),
});
};

View File

@@ -0,0 +1,33 @@
{
"name": "@vercel/gatsby-plugin-vercel-builder",
"version": "0.1.2",
"main": "dist/index.js",
"files": [
"dist",
"gatsby-node.ts",
"gatsby-node.js",
"gatsby-node.js.map"
],
"scripts": {
"build": "pnpm build:src && pnpm build:gatsby",
"build:gatsby": "tsc -p tsconfig.gatsby.json",
"build:src": "tsc -p tsconfig.src.json"
},
"dependencies": {
"@vercel/build-utils": "5.9.0",
"@vercel/node": "2.8.15",
"@vercel/routing-utils": "2.1.8",
"ajv": "8.12.0",
"esbuild": "0.16.17",
"etag": "1.8.1",
"fs-extra": "11.1.0"
},
"devDependencies": {
"@types/etag": "1.8.0",
"@types/fs-extra": "11.0.1",
"@types/node": "14.18.33",
"@types/react": "18.0.26",
"gatsby": "4.25.2",
"typescript": "4.3.4"
}
}

View File

@@ -0,0 +1,129 @@
import { join } from 'path';
import { getNodeVersion } from '@vercel/build-utils';
import { build } from 'esbuild';
import {
copy,
copyFile,
pathExists,
writeJson,
writeFileSync,
ensureFileSync,
} from 'fs-extra';
import type {
NodejsServerlessFunctionConfig,
PrerenderFunctionConfig,
} from './../types';
export const writeHandler = async ({
outDir,
handlerFile,
}: {
outDir: string;
handlerFile: string;
}) => {
const { major } = await getNodeVersion(process.cwd());
try {
return await build({
entryPoints: [handlerFile],
loader: { '.ts': 'ts' },
outfile: join(outDir, './index.js'),
format: 'cjs',
target: `node${major}`,
platform: 'node',
bundle: true,
minify: true,
define: {
'process.env.NODE_ENV': "'production'",
},
});
} catch (e: any) {
console.error('Failed to build lambda handler', e.message);
}
};
export const writeVCConfig = async ({
functionDir,
handler = 'index.js',
}: {
functionDir: string;
handler?: string;
}) => {
const { runtime } = await getNodeVersion(process.cwd());
const config: NodejsServerlessFunctionConfig = {
runtime,
handler,
launcherType: 'Nodejs',
shouldAddHelpers: true,
};
return writeJson(`${functionDir}/.vc-config.json`, config);
};
export const writePrerenderConfig = (outputPath: string) => {
const config: PrerenderFunctionConfig = {
expiration: false,
};
ensureFileSync(outputPath);
return writeFileSync(outputPath, JSON.stringify(config));
};
export async function movePageData({ functionDir }: { functionDir: string }) {
await copy(
join('.vercel', 'output', 'static', 'page-data'),
join(functionDir, 'page-data')
);
}
export async function copyFunctionLibs({
functionDir,
}: {
functionDir: string;
}) {
/* Copies the required libs for Serverless Functions from .cache to the <name>.func folder */
await Promise.allSettled(
[
{
src: join('.cache', 'query-engine'),
dest: join(functionDir, '.cache', 'query-engine'),
},
{
src: join('.cache', 'page-ssr'),
dest: join(functionDir, '.cache', 'page-ssr'),
},
// {
// src: join(functionDir, '.cache', 'query-engine', 'assets'),
// dest: join(functionDir, 'assets'),
// },
{
src: join('.cache', 'data', 'datastore'),
dest: join(functionDir, '.cache', 'data', 'datastore'),
},
{
src: join('.cache', 'caches'),
dest: join(functionDir, '.cache', 'caches'),
},
].map(({ src, dest }) => copy(src, dest))
);
}
export async function copyHTMLFiles({ functionDir }: { functionDir: string }) {
/* If available, copies the 404.html and 500.html files to the <name>.func/lib folder */
for (const htmlFile of ['404', '500']) {
if (await pathExists(join('public', `${htmlFile}.html`))) {
try {
await copyFile(
join('public', `${htmlFile}.html`),
join(functionDir, `${htmlFile}.html`)
);
} catch (e: any) {
console.error('Failed to copy HTML files', e.message);
process.exit(1);
}
}
}
}

View File

@@ -0,0 +1,58 @@
import os from 'os';
import { join } from 'path';
import etag from 'etag';
import { copySync, existsSync, readFileSync } from 'fs-extra';
import type { VercelRequest, VercelResponse } from '@vercel/node';
import { getGraphQLEngine, getPageSSRHelpers } from '../utils';
const TMP_DATA_PATH = join(os.tmpdir(), 'data/datastore');
const CUR_DATA_PATH = join(__dirname, '.cache/data/datastore');
if (!existsSync(TMP_DATA_PATH)) {
// Copies executable `data` files to the writable /tmp directory.
copySync(CUR_DATA_PATH, TMP_DATA_PATH);
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const splitPathName = req.url!.split('/')[2];
const pathName = splitPathName === `index` ? `/` : splitPathName;
if (
existsSync(join(__dirname, 'page-data', splitPathName, 'page-data.json'))
) {
/* Non-SSR/DSG pages already have a pre-generated page-data.json file.
Instead of generating this dynamically, we can directly serve this JSON. */
res.setHeader('Content-Type', 'application/json');
return res
.status(200)
.json(
readFileSync(
join(__dirname, 'page-data', splitPathName, 'page-data.json'),
'utf-8'
)
);
}
const { getData, renderPageData } = await getPageSSRHelpers();
const graphqlEngine = await getGraphQLEngine();
const data = await getData({
req,
graphqlEngine,
pathName,
});
const pageData = await renderPageData({ data });
if (data.serverDataHeaders) {
for (const [name, value] of Object.entries(data.serverDataHeaders)) {
res.setHeader(name, value);
}
}
res.setHeader('ETag', etag(JSON.stringify(pageData)));
return res.json(pageData);
}

View File

@@ -0,0 +1,40 @@
import { join } from 'path';
import os from 'os';
import { copySync, existsSync } from 'fs-extra';
import { getPageSSRHelpers, getGraphQLEngine } from '../utils';
import type { VercelRequest, VercelResponse } from '@vercel/node';
const TMP_DATA_PATH = join(os.tmpdir(), 'data/datastore');
const CUR_DATA_PATH = join(__dirname, '.cache/data/datastore');
if (!existsSync(TMP_DATA_PATH)) {
// Copies executable `data` files to the writable /tmp directory.
copySync(CUR_DATA_PATH, TMP_DATA_PATH);
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
const graphqlEngine = await getGraphQLEngine();
const { getData, renderHTML } = await getPageSSRHelpers();
const data = await getData({
pathName: req.url as string,
graphqlEngine,
req,
});
const results = await renderHTML({ data });
if (data.serverDataHeaders) {
for (const [name, value] of Object.entries(data.serverDataHeaders)) {
res.setHeader(name, value);
}
}
if (data.serverDataStatus) {
res.statusCode = data.serverDataStatus;
}
res.send(results);
}

View File

@@ -0,0 +1,20 @@
import { join } from 'path';
import os from 'os';
const TMP_DATA_PATH = join(os.tmpdir(), 'data/datastore');
export async function getGraphQLEngine() {
const { GraphQLEngine } = (await import(
join(__dirname, '.cache/query-engine/index.js')
)) as typeof import('gatsby/dist/schema/graphql-engine/entry');
return new GraphQLEngine({ dbPath: TMP_DATA_PATH });
}
export async function getPageSSRHelpers() {
const { getData, renderPageData, renderHTML } = (await import(
join(__dirname, '.cache/page-ssr/index.js')
)) as typeof import('gatsby/dist/utils/page-ssr-module/entry');
return { getData, renderPageData, renderHTML };
}

View File

@@ -0,0 +1,126 @@
import { join } from 'path';
import { ensureDir } from 'fs-extra';
import { createSymlink } from '../utils/symlink';
import {
writeHandler,
writeVCConfig,
copyFunctionLibs,
movePageData,
copyHTMLFiles,
writePrerenderConfig,
} from '../handlers/build';
import { GatsbyFunction } from '../schemas';
import { Routes } from '../types';
export async function createServerlessFunctions({
dsgRoutes,
ssrRoutes,
}: Routes) {
/* Gatsby SSR/DSG on Vercel is enabled through Vercel Serverless Functions.
This plugin creates one Serverless Function called `_ssr.func` that is used by SSR and DSG pages through symlinks.
DSG is enabled through prerender functions.
*/
const functionName = '_ssr.func';
const functionDir = join('.vercel', 'output', 'functions', functionName);
const handlerFile = join(
__dirname,
'..',
'handlers',
'templates',
'./ssr-handler.js'
);
await ensureDir(functionDir);
await Promise.all([
writeHandler({ outDir: functionDir, handlerFile }),
copyFunctionLibs({ functionDir }),
copyHTMLFiles({ functionDir }),
writeVCConfig({ functionDir }),
]);
await Promise.all([
...ssrRoutes.map(async (pathName: string) => {
return createSymlink(pathName, functionName);
}),
...dsgRoutes.map(async (pathName: string) => {
writePrerenderConfig(
join(
'.vercel',
'output',
'functions',
`${pathName}.prerender-config.json`
)
);
return createSymlink(pathName, functionName);
}),
]);
}
export async function createPageDataFunction({ dsgRoutes, ssrRoutes }: Routes) {
/* Gatsby uses /page-data/<path>/page-data.json to fetch data. This plugin creates a
`_page-data.func` function that dynamically generates this data if it's not available in `static/page-data`. */
const functionName = '_page-data.func';
const functionDir = join('.vercel', 'output', 'functions', functionName);
const handlerFile = join(
__dirname,
'..',
'handlers',
'templates',
'./page-data.js'
);
await ensureDir(functionDir);
await Promise.all([
writeHandler({ outDir: functionDir, handlerFile }),
copyFunctionLibs({ functionDir }),
movePageData({ functionDir }),
writeVCConfig({ functionDir }),
]);
await Promise.all([
...ssrRoutes.map(async (pathName: string) => {
return createSymlink(
`page-data/${pathName}/page-data.json`,
functionName
);
}),
...dsgRoutes.map(async (pathName: string) => {
const funcPath = `page-data/${pathName}/page-data.json`;
writePrerenderConfig(
join(
'.vercel',
'output',
'functions',
`${funcPath}.prerender-config.json`
)
);
return createSymlink(funcPath, functionName);
}),
]);
}
export async function createAPIRoutes(functions: GatsbyFunction[]) {
const apiDir = join('.vercel', 'output', 'functions', 'api');
await ensureDir(apiDir);
await Promise.allSettled(
functions.map(async (func: GatsbyFunction) => {
const apiRouteDir = `${apiDir}/${func.functionRoute}.func`;
const handlerFile = func.originalAbsoluteFilePath;
await ensureDir(apiRouteDir);
await Promise.all([
writeHandler({ outDir: apiRouteDir, handlerFile }),
writeVCConfig({ functionDir: apiRouteDir }),
]);
})
);
}

View File

@@ -0,0 +1,10 @@
import { join } from 'path';
import { copy, ensureDir } from 'fs-extra';
export async function createStaticDir() {
const targetDir = join(process.cwd(), '.vercel', 'output', 'static');
await ensureDir(targetDir);
await copy(join(process.cwd(), 'public'), targetDir);
}

View File

@@ -0,0 +1,104 @@
import { join } from 'path';
import { getTransformedRoutes } from '@vercel/routing-utils';
import { pathExists, writeJson, remove, mkdirp } from 'fs-extra';
import { validateGatsbyState } from './schemas';
import {
createServerlessFunctions,
createPageDataFunction,
createAPIRoutes,
} from './helpers/functions';
import { createStaticDir } from './helpers/static';
export interface GenerateVercelBuildOutputAPI3OutputOptions {
exportPath: string;
gatsbyStoreState: {
pages: Map<string, unknown>;
redirects: unknown;
functions: unknown;
};
[x: string]: unknown;
}
import type { Config, Routes } from './types';
export async function generateVercelBuildOutputAPI3Output({
exportPath,
gatsbyStoreState,
}: GenerateVercelBuildOutputAPI3OutputOptions) {
const state = {
pages: Array.from(gatsbyStoreState.pages.entries()), // must transform from a Map for validation
redirects: gatsbyStoreState.redirects,
functions: gatsbyStoreState.functions,
};
if (validateGatsbyState(state)) {
console.log('▲ Creating Vercel build output');
await remove(join('.vercel', 'output'));
const { pages, redirects, functions } = state;
const { ssrRoutes, dsgRoutes } = pages.reduce<Routes>(
(acc, [, cur]) => {
if (cur.mode === 'SSR') {
acc.ssrRoutes.push(cur.path);
} else if (cur.mode === 'DSG') {
acc.dsgRoutes.push(cur.path);
}
return acc;
},
{
ssrRoutes: [],
dsgRoutes: [],
}
);
await createStaticDir();
await mkdirp(join('.cache', 'caches'));
const createPromises: Promise<void>[] = [];
if (functions.length > 0) createPromises.push(createAPIRoutes(functions));
if (ssrRoutes.length > 0 || dsgRoutes.length > 0) {
createPromises.push(createPageDataFunction({ ssrRoutes, dsgRoutes }));
createPromises.push(createServerlessFunctions({ ssrRoutes, dsgRoutes }));
}
await Promise.all(createPromises);
const vercelConfigPath = `${process.cwd()}/vercel.config.js`;
const vercelConfig: Config = (await pathExists(vercelConfigPath))
? require(vercelConfigPath).default
: {};
const { routes } = getTransformedRoutes({
...vercelConfig,
trailingSlash: false,
redirects: redirects.map(({ fromPath, toPath, isPermanent }) => ({
source: fromPath,
destination: toPath,
permanent: isPermanent,
})),
rewrites: [
{
source: '^/page-data(?:/(.*))/page-data\\.json$',
destination: '/_page-data',
},
],
});
const config: Config = {
version: 3,
routes: routes || undefined,
};
await writeJson(exportPath, config);
console.log('Vercel output has been generated');
} else {
throw new Error(
'Gatsby state validation error. Please file an issue https://vercel.com/help#issues'
);
}
}

View File

@@ -0,0 +1,82 @@
import type {
IGatsbyPage,
IGatsbyFunction,
IRedirect,
} from 'gatsby/dist/redux/types';
import Ajv, { JSONSchemaType } from 'ajv';
export type GatsbyPage = Pick<IGatsbyPage, 'mode' | 'path'>;
const GatsbyPageSchema: JSONSchemaType<GatsbyPage> = {
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['SSG', 'DSG', 'SSR'],
},
path: {
type: 'string',
},
},
required: ['mode', 'path'],
} as const;
export interface GatsbyState {
pages: Array<[string, GatsbyPage]>;
redirects: Array<GatsbyRedirect>;
functions: Array<GatsbyFunction>;
}
export type GatsbyFunction = Pick<
IGatsbyFunction,
'functionRoute' | 'originalAbsoluteFilePath'
>;
const GatsbyFunctionSchema: JSONSchemaType<GatsbyFunction> = {
type: 'object',
properties: {
functionRoute: { type: 'string' },
originalAbsoluteFilePath: { type: 'string' },
},
required: ['functionRoute', 'originalAbsoluteFilePath'],
} as const;
export type GatsbyRedirect = Pick<
IRedirect,
'fromPath' | 'toPath' | 'isPermanent'
>;
const GatsbyRedirectSchema: JSONSchemaType<GatsbyRedirect> = {
type: 'object',
properties: {
fromPath: { type: 'string' },
toPath: { type: 'string' },
isPermanent: { type: 'boolean', nullable: true },
},
required: ['fromPath', 'toPath'],
} as const;
const GatsbyStateSchema: JSONSchemaType<GatsbyState> = {
type: 'object',
properties: {
pages: {
type: 'array',
items: {
type: 'array',
minItems: 2,
maxItems: 2,
items: [{ type: 'string' }, GatsbyPageSchema],
},
},
redirects: {
type: 'array',
items: GatsbyRedirectSchema,
},
functions: {
type: 'array',
items: GatsbyFunctionSchema,
},
},
required: ['pages', 'redirects', 'functions'],
additionalProperties: true,
} as const;
export const ajv = new Ajv({ allErrors: true });
export const validateGatsbyState = ajv.compile(GatsbyStateSchema);

View File

@@ -0,0 +1,126 @@
import { GatsbyPage } from './schemas';
export type Config = {
version: 3;
routes?: Route[];
images?: ImagesConfig;
wildcard?: WildcardConfig;
overrides?: OverrideConfig;
cache?: string[];
};
type Route = Source | Handler;
type Source = {
src: string;
dest?: string;
headers?: Record<string, string>;
methods?: string[];
continue?: boolean;
caseSensitive?: boolean;
check?: boolean;
status?: number;
has?: Array<HostHasField | HeaderHasField | CookieHasField | QueryHasField>;
missing?: Array<
HostHasField | HeaderHasField | CookieHasField | QueryHasField
>;
locale?: Locale;
middlewarePath?: string;
};
type Locale = {
redirect?: Record<string, string>;
cookie?: string;
};
type HostHasField = {
type: 'host';
value: string;
};
type HeaderHasField = {
type: 'header';
key: string;
value?: string;
};
type CookieHasField = {
type: 'cookie';
key: string;
value?: string;
};
type QueryHasField = {
type: 'query';
key: string;
value?: string;
};
type HandleValue =
| 'rewrite'
| 'filesystem' // check matches after the filesystem misses
| 'resource'
| 'miss' // check matches after every filesystem miss
| 'hit'
| 'error'; // check matches after error (500, 404, etc.)
type Handler = {
handle: HandleValue;
src?: string;
dest?: string;
status?: number;
};
type ImageFormat = 'image/avif' | 'image/webp';
type ImagesConfig = {
sizes: number[];
domains: string[];
minimumCacheTTL?: number; // seconds
formats?: ImageFormat[];
dangerouslyAllowSVG?: boolean;
contentSecurityPolicy?: string;
};
type WildCard = {
domain: string;
value: string;
};
type WildcardConfig = Array<WildCard>;
type Override = {
path?: string;
contentType?: string;
};
type OverrideConfig = Record<string, Override>;
type ServerlessFunctionConfig = {
handler: string;
runtime: string;
memory?: number;
maxDuration?: number;
environment?: Record<string, string>[];
allowQuery?: string[];
regions?: string[];
};
export type NodejsServerlessFunctionConfig = ServerlessFunctionConfig & {
launcherType: 'Nodejs';
shouldAddHelpers?: boolean; // default: false
shouldAddSourceMapSupport?: boolean; // default: false
};
export type PrerenderFunctionConfig = {
expiration: number | false;
group?: number;
bypassToken?: string;
fallback?: string;
allowQuery?: string[];
};
export interface Routes {
ssrRoutes: Array<GatsbyPage['path']>;
dsgRoutes: Array<GatsbyPage['path']>;
}

View File

@@ -0,0 +1,20 @@
import path, { join, normalize, sep } from 'path';
import { ensureDir, symlinkSync } from 'fs-extra';
const removeTrailingSlash = (str: string) => str.replace(/\/$/, '');
export const createSymlink = async (pathName: string, destName: string) => {
const functionName = removeTrailingSlash(pathName).split(sep).pop();
const dirPath = removeTrailingSlash(
join('.vercel', 'output', 'functions', normalize(join(pathName, '..')))
);
await ensureDir(dirPath);
symlinkSync(
path.relative(dirPath, join('.vercel', 'output', 'functions', destName)),
path.join(dirPath, `${functionName}.func`)
);
};

View File

@@ -0,0 +1,104 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
"declarationMap": true /* Create sourcemaps for d.ts files. */,
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": []
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"declaration": false,
"declarationMap": false
},
"include": ["gatsby-node.ts"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "2.2.25",
"version": "2.2.30",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
@@ -36,7 +36,7 @@
"@types/node": "14.18.33",
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/build-utils": "5.9.0",
"@vercel/ncc": "0.24.0",
"async-retry": "1.3.1",
"execa": "^1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/hydrogen",
"version": "0.0.39",
"version": "0.0.44",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -21,8 +21,8 @@
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "14.18.33",
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/static-config": "workspace:2.0.7",
"@vercel/build-utils": "5.9.0",
"@vercel/static-config": "2.0.11",
"execa": "3.2.0",
"fs-extra": "11.1.0",
"ts-morph": "12.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.3.10",
"version": "3.3.18",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -45,9 +45,9 @@
"@types/semver": "6.0.0",
"@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "workspace:5.7.6",
"@vercel/build-utils": "5.9.0",
"@vercel/nft": "0.22.5",
"@vercel/routing-utils": "workspace:2.1.4",
"@vercel/routing-utils": "2.1.8",
"async-sema": "3.0.1",
"buffer-crc32": "0.2.13",
"bytes": "3.1.2",

View File

@@ -214,7 +214,7 @@ export const build: BuildV2 = async ({
repoRootPath = entryPath;
entryPath = path.join(entryPath, config.rootDirectory as string);
}
const outputDirectory = config.outputDirectory || '.next';
const outputDirectory = path.join('./', config.outputDirectory || '.next');
const dotNextStatic = path.join(entryPath, outputDirectory, 'static');
// TODO: remove after testing used for simulating root directory monorepo
// setting that can't be triggered with vercel.json
@@ -2595,7 +2595,7 @@ export const prepareCache: PrepareCache = async ({
debug('Preparing cache...');
const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
const outputDirectory = config.outputDirectory || '.next';
const outputDirectory = path.join('./', config.outputDirectory || '.next');
const nextVersionRange = await getNextVersionRange(entryPath);
const isLegacy = nextVersionRange && isLegacyNext(nextVersionRange);

View File

@@ -1060,9 +1060,9 @@ export async function serverBuild({
{
src: path.posix.join(
'^/',
entryDirectory,
trailingSlash ? '/' : '',
'$'
entryDirectory !== '.'
? `${entryDirectory}${trailingSlash ? '/$' : '$'}`
: '$'
),
has: [
{
@@ -1417,7 +1417,7 @@ export async function serverBuild({
src: `^${path.posix.join(
'/',
entryDirectory,
'/((?!.+\\.rsc).+)$'
'/((?!.+\\.rsc).+?)(?:/)?$'
)}`,
has: [
{

View File

@@ -0,0 +1,12 @@
/* eslint-env jest */
const path = require('path');
const { deployAndTest } = require('../../utils');
const ctx = {};
describe(`${__dirname.split(path.sep).pop()}`, () => {
it('should deploy and pass probe checks', async () => {
const info = await deployAndTest(__dirname);
Object.assign(ctx, info);
});
});

View File

@@ -0,0 +1,12 @@
{
"name": "test-root-dir",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@next/font": "13.1.2",
"next": "13.1.2",
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
module.exports = nextConfig;

View File

@@ -0,0 +1,12 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {}
}

View File

@@ -0,0 +1,9 @@
import { NextResponse } from 'next/server';
export default function handler(req) {
return NextResponse.json({ hello: 'world', now: Date.now(), url: req.url });
}
export const config = {
runtime: 'edge',
};

View File

@@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' });
}

View File

@@ -0,0 +1,3 @@
export default function Index() {
return <p>Index page</p>;
}

View File

@@ -0,0 +1,20 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next",
"config": {
"buildCommand": "cd packages/client && yarn build",
"outputDirectory": "/packages/client/.next"
}
}
],
"probes": [
{
"path": "/api/data/first",
"status": 200,
"mustContain": "\"hello\":\"world\""
}
]
}

View File

@@ -0,0 +1,7 @@
export default function AnotherPage(props) {
return (
<>
<p>hello from newroot/dashboard/another</p>
</>
);
}

View File

@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html className="this-is-another-document-html">
<head>
<title>{`hello world`}</title>
</head>
<body className="this-is-another-document-body">{children}</body>
</html>
);
}

View File

@@ -0,0 +1,7 @@
export default function ChangelogPage(props) {
return (
<>
<p>hello from app/dashboard/changelog</p>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function HelloPage(props) {
return (
<>
<p>hello from app/dashboard/rootonly/hello</p>
</>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { useState, useEffect } from 'react';
import style from './style.module.css';
import './style.css';
export default function ClientComponentRoute() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(1);
}, [count]);
return (
<>
<p className={style.red}>
hello from app/client-component-route. <b>count: {count}</b>
</p>
</>
);
}

View File

@@ -0,0 +1,3 @@
b {
color: blue;
}

View File

@@ -0,0 +1,3 @@
.red {
color: red;
}

View File

@@ -0,0 +1,20 @@
'use client';
import { useState, useEffect } from 'react';
import styles from './style.module.css';
import './style.css';
export default function ClientNestedLayout({ children }) {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(1);
}, []);
return (
<>
<h1 className={styles.red}>Client Nested. Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function ClientPage() {
return (
<>
<p>hello from app/client-nested</p>
</>
);
}

View File

@@ -0,0 +1,3 @@
button {
color: red;
}

View File

@@ -0,0 +1,3 @@
.red {
color: red;
}

View File

@@ -0,0 +1,7 @@
export default function DeploymentsBreakdownPage(props) {
return (
<>
<p>hello from app/dashboard/(custom)/deployments/breakdown</p>
</>
);
}

View File

@@ -0,0 +1,8 @@
export default function CustomDashboardRootLayout({ children }) {
return (
<>
<h2>Custom dashboard</h2>
{children}
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function DeploymentsPage(props) {
return (
<>
<p>hello from app/dashboard/deployments/[id]. ID is: {props.params.id}</p>
</>
);
}

View File

@@ -0,0 +1,10 @@
export default function DeploymentsPage(props) {
return (
<>
<p>
hello from app/dashboard/deployments/[id]/settings. ID is:{' '}
{props.params.id}
</p>
</>
);
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <p>catchall</p>;
}

View File

@@ -0,0 +1,7 @@
export default function DeploymentsInfoPage(props) {
return (
<>
<p>hello from app/dashboard/deployments/info</p>
</>
);
}

View File

@@ -0,0 +1,8 @@
export default function DeploymentsLayout({ message, children }) {
return (
<>
<h2>Deployments hello</h2>
{children}
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function IntegrationsPage(props) {
return (
<>
<p>hello from app/dashboard/integrations</p>
</>
);
}

View File

@@ -0,0 +1,8 @@
export default function DashboardLayout(props) {
return (
<>
<h1>Dashboard</h1>
{props.children}
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function DashboardPage(props) {
return (
<>
<p>hello from app/dashboard</p>
</>
);
}

View File

@@ -0,0 +1,11 @@
export default function IdLayout({ children, params }) {
return (
<>
<h3>
Id Layout. Params:{' '}
<span id="id-layout-params">{JSON.stringify(params)}</span>
</h3>
{children}
</>
);
}

View File

@@ -0,0 +1,11 @@
export default function IdPage({ children, params }) {
return (
<>
<p>
Id Page. Params:{' '}
<span id="id-page-params">{JSON.stringify(params)}</span>
</p>
{children}
</>
);
}

View File

@@ -0,0 +1,11 @@
export default function CategoryLayout({ children, params }) {
return (
<>
<h2>
Category Layout. Params:{' '}
<span id="category-layout-params">{JSON.stringify(params)}</span>{' '}
</h2>
{children}
</>
);
}

View File

@@ -0,0 +1,11 @@
export default function DynamicLayout({ children, params }) {
return (
<>
<h1>
Dynamic Layout. Params:{' '}
<span id="dynamic-layout-params">{JSON.stringify(params)}</span>
</h1>
{children}
</>
);
}

View File

@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html className="this-is-the-document-html">
<head>
<title>{`hello world`}</title>
</head>
<body className="this-is-the-document-body">{children}</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <p>index app page</p>;
}

View File

@@ -0,0 +1,7 @@
export default function DeploymentsPage(props) {
return (
<>
<p>hello from app/partial-match-[id]. ID is: {props.params.id}</p>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function SharedComponentRoute() {
return (
<>
<p>hello from app/shared-component-route</p>
</>
);
}

View File

@@ -0,0 +1,9 @@
'use client';
export default function ShouldNotServeClientDotJs(props) {
return (
<>
<p>hello from app/should-not-serve-client</p>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function ShouldNotServeServerDotJs(props) {
return (
<>
<p>hello from app/should-not-serve-server</p>
</>
);
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return <p id="loading-layout">Loading layout...</p>;
}

View File

@@ -0,0 +1,18 @@
import { use } from 'react';
async function getData() {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
message: 'hello from slow layout',
};
}
export default function SlowLayout(props) {
const data = use(getData());
return (
<>
<p id="slow-layout-message">{data.message}</p>
{props.children}
</>
);
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return <p id="loading-page">Loading page...</p>;
}

View File

@@ -0,0 +1,13 @@
import { use } from 'react';
async function getData() {
await new Promise(resolve => setTimeout(resolve, 5000));
return {
message: 'hello from slow page',
};
}
export default function SlowPage(props) {
const data = use(getData());
return <h1 id="slow-page-message">{data.message}</h1>;
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return <p id="loading">Loading...</p>;
}

View File

@@ -0,0 +1,18 @@
import { use } from 'react';
async function getData() {
await new Promise(resolve => setTimeout(resolve, 5000));
return {
message: 'hello from slow layout',
};
}
export default function SlowLayout(props) {
const data = use(getData());
return (
<>
<p id="slow-layout-message">{data.message}</p>
{props.children}
</>
);
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <h1 id="page-message">Hello World</h1>;
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return <p id="loading">Loading...</p>;
}

Some files were not shown because too many files have changed in this diff Show More