Compare commits

...

50 Commits

Author SHA1 Message Date
Steven
7d7f3df980 Publish Stable
- @vercel/python@2.1.1
2021-11-23 09:04:08 -05:00
Steven
5cf0c316e9 Publish Stable
- @vercel/python@2.1.0
2021-11-23 08:57:58 -05:00
Steven
f4501433c8 Publish Canary
- vercel@23.1.3-canary.38
 - vercel-plugin-python@1.0.0-canary.6
 - @vercel/python@2.0.6-canary.7
2021-11-22 18:43:37 -05:00
Steven
19831593ce [python] Add warning for deprecated version (#7053)
- Print warning when python 3.6 is selected
- Print warning when invalid python version is selected
2021-11-22 23:36:30 +00:00
Steven
5d85bb1426 [tests] Bump cancel action to 0.9.1 (#7055) 2021-11-22 17:24:35 -05:00
Andy Bitz
f194d54b0c Publish Canary
- vercel@23.1.3-canary.37
2021-11-22 23:14:32 +01:00
Andy
6542086843 Revert "[cli] Switch from hardlinks to symlinks in vc build" (#7054)
* Revert "[cli] Switch from hardlinks to symlinks in vc build (#7026)"

This reverts commit 6dff0875f5.

* Keep copy logic
2021-11-22 23:12:46 +01:00
Andy Bitz
2721b3449d Publish Canary
- @vercel/build-utils@2.12.3-canary.20
 - vercel@23.1.3-canary.36
 - @vercel/client@10.2.3-canary.21
 - @vercel/node@1.12.2-canary.7
 - vercel-plugin-go@1.0.0-canary.5
 - vercel-plugin-node@1.12.2-canary.11
 - vercel-plugin-python@1.0.0-canary.5
 - vercel-plugin-ruby@1.0.0-canary.4
2021-11-22 13:22:56 +01:00
Andy
adb284519a [plugin-node][node] Fix dependency installation, handler path, and build script (#7043)
* [plugin-node][node] Fix dependency installation, handler path, and build script

* Use smaller library for testing

* Make test work on windows

* Remove .output before fixture runs and update routes update

* Apply suggestions from code review

Co-authored-by: Steven <steven@ceriously.com>

Co-authored-by: Steven <steven@ceriously.com>
2021-11-22 13:21:20 +01:00
Steven
b2d91f3121 [cli] Fix metrics crash on some platforms (#7034) 2021-11-20 20:31:39 -05:00
Steven
32664cd13b Publish Canary
- @vercel/build-utils@2.12.3-canary.19
 - vercel@23.1.3-canary.35
 - @vercel/client@10.2.3-canary.20
 - @vercel/frameworks@0.5.1-canary.12
 - vercel-plugin-go@1.0.0-canary.4
 - vercel-plugin-node@1.12.2-canary.10
 - vercel-plugin-python@1.0.0-canary.4
 - vercel-plugin-ruby@1.0.0-canary.3
2021-11-20 10:20:05 -05:00
Sam Ko
db468c489a [frameworks] Fix Parcel default output directory to dist (#7029)
### Related Issues

The Parcel template via the Project Creating Flow returns a build error because the default output directory for the Parcel **Framework Preset** is searching for `public` instead of `dist`. The default behavior of Parcel's CLI I believe is `dist`.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-11-20 01:44:22 +00:00
Steven
edd9bb506c [frameworks][cli] Consolidate buildCommand and devCommand (#7032)
* [frameworks] Consolidate buildCommand and devCommand

* Fix default build command when no override or package.json found
2021-11-19 19:07:52 -05:00
Steven
a72549a290 [tests] Make test more reliable for function timeout (#7033) 2021-11-19 17:26:32 -05:00
Steven
4aa6a13912 Publish Canary
- vercel@23.1.3-canary.34
 - vercel-plugin-python@1.0.0-canary.3
 - @vercel/python@2.0.6-canary.6
2021-11-19 14:42:02 -05:00
Steven
81ea0082f1 [python] Upgrade python 3.9 (#7008)
* [python] Upgrade python 3.9

* Fix python path

* Improve testing

* Add support for version selection via Pipfile

* Parse the "Pipfile.lock" instead

* Fix support for Node.js 12
2021-11-19 13:42:17 -05:00
Jared Palmer
6dff0875f5 [cli] Switch from hardlinks to symlinks in vc build (#7026)
* Switch from hardlinks to symlinks in vc build

* Remove error handling that fs-extra already takes care of

* Add comment

* Put back symlink error handling
2021-11-19 12:36:31 -05:00
Steven
30aa392c0a [tests] Increase CI duration (#7030)
Seems like macOS sometimes exceeds 60 min so this bumps the timeout.

The build can be between 3 minutes and 5 minutes. The tests can be between 45 minutes and 55 minutes.
2021-11-19 14:59:12 +00:00
Miroslav Simulcik
c4fc060030 [cli] Fix spinner output outside of TTY (#7017)
* Fix spinner output outside of TTY

* Use console.log

* Fix tests

* Revert "Fix tests"

This reverts commit 44383eea2dc3bb699d943079d77961ed86fe65d6.

* Revert "Use console.log"

This reverts commit 251bd1d624532e9ecfb4aee5b31538aa4ba740ef.
2021-11-17 14:04:59 +01:00
Steven
3fa08bf64f [cli] Fix update command to favor npm (#7018) 2021-11-16 16:07:31 -05:00
Steven
43056bde1f Publish Canary
- vercel@23.1.3-canary.33
2021-11-15 18:07:17 -05:00
Steven
a49966b9b4 [cli] Change vc build to use stdout (#7005) 2021-11-15 18:06:48 -05:00
Steven
7f55de71bb Publish Canary
- vercel@23.1.3-canary.32
 - @vercel/go@1.2.4-canary.4
 - vercel-plugin-go@1.0.0-canary.3
2021-11-15 13:37:04 -05:00
Steven
db8e36e04c [go] Add support for version 1.17 (#7002)
* Support more Go versions

Co-authored-by: Stefan M <StefMa@users.noreply.github.com>
2021-11-15 13:36:26 -05:00
Andy Bitz
82924bb5c4 Publish Canary
- vercel@23.1.3-canary.31
 - vercel-plugin-node@1.12.2-canary.9
2021-11-15 14:11:55 +01:00
Andy
18b5fac93e [vercel-plugin-node] Use .nft.json files instead of directories. (#6988)
* Ignore install step if package.json does not exist in entrypoint directory

* [plugin-node] Use `.nft.json` files

* Bump @vercel/fun

* Update package.json

* Remove package

* Remove import

* Update paths

* Revert "Update paths"

This reverts commit fa304e90381f475c64b2333402dca489813522da.

* Update path for windows

* Update handler path
2021-11-15 14:08:49 +01:00
Miroslav Simulcik
a6012e600b [cli] Fix updating of gitignore in vercel link (#6986)
* Fix updating of gitignore in link

* Fix modification of gitignore file during linking

* Fix empty file case
2021-11-12 17:11:21 +01:00
Steven
c3abf73f58 Publish Canary
- @vercel/build-utils@2.12.3-canary.18
 - vercel@23.1.3-canary.30
 - @vercel/client@10.2.3-canary.19
 - vercel-plugin-go@1.0.0-canary.2
 - vercel-plugin-node@1.12.2-canary.8
 - vercel-plugin-python@1.0.0-canary.2
 - vercel-plugin-ruby@1.0.0-canary.2
2021-11-11 14:54:33 -05:00
Steven
4873b8b379 [build-utils][node] Add updateFunctionsManifest function (#6971) 2021-11-11 14:53:43 -05:00
Miroslav Simulcik
6248139281 [cli] Prevent recursion of vercel build and vercel dev (#6960)
* Prevent infinite recursion of vercel build and dev

* Improved text

* Improved the text further

* Add tests

* Debug test

* Add debug log

* Debug test

* debug test

* Debug test

* Fix test

* Debug tests

* Debug test

* Remove only from tests

* Add error documentation

* Improved docs

* Fix other test

* Fixed links to error docs

* Remove legacy error page

* Use nicer error links

Co-authored-by: Leo Lamprecht <leo@vercel.com>
2021-11-11 10:51:30 -05:00
Miroslav Simulcik
507a5de3cd [tests] Fix getting of config paths (#6969)
Co-authored-by: Steven <steven@ceriously.com>
2021-11-09 14:34:41 -05:00
Gary Borton
be1c78e72f Publish Canary
- vercel@23.1.3-canary.29
 - vercel-plugin-middleware@0.0.0-canary.7
2021-11-09 11:07:01 -08:00
Gary Borton
c277c649c6 Fix middleware build result. Return middleware next when invalid result. (#6963)
* Fix middleware build result. Return middleware next when invalid result.

* Add a unit test that would have failed before previous commit.

* Copy next.js behavior.

* Add a test for invalid responses from middleware.

Co-authored-by: Leo Lamprecht <leo@vercel.com>
2021-11-09 11:06:16 -08:00
Steven
ed1dacd276 Publish Canary
- @vercel/build-utils@2.12.3-canary.17
 - vercel@23.1.3-canary.28
 - @vercel/client@10.2.3-canary.18
 - vercel-plugin-go@1.0.0-canary.1
 - vercel-plugin-python@1.0.0-canary.1
 - vercel-plugin-ruby@1.0.0-canary.1
 - @vercel/python@2.0.6-canary.5
 - @vercel/ruby@1.2.8-canary.4
2021-11-09 12:50:34 -05:00
Steven
144e890bfa Add plugin packages for go/python/ruby (#6961)
* Add plugin packages

* Fix usage

* Fix build

* Fix workspace linking to build-utils
2021-11-09 12:48:40 -05:00
Andy Bitz
af097c2c06 Publish Canary
- vercel@23.1.3-canary.27
2021-11-09 16:44:14 +01:00
Andy
873a582986 [cli] Ignore .next/cache in vc build (#6968)
* [cli] Ignore .next/cache in `vc build`

* Handle middleware-manifest.json

* Update manifest update
2021-11-09 16:43:26 +01:00
Andy Bitz
986b4c0b1a Publish Canary
- vercel@23.1.3-canary.26
2021-11-09 01:31:30 +01:00
Andy
14071819ac [cli] Fix NFT output path for monorepos (#6965)
* [cli] Fix `output` path for `.output` for `node_modules`

* Use baseDir instead of cwd

* Update comment

* Update output for requiredServerFiles
2021-11-09 01:30:49 +01:00
jj@jjsweb.site
2a8588a0c5 Publish Canary
- @vercel/build-utils@2.12.3-canary.16
 - vercel@23.1.3-canary.25
 - @vercel/client@10.2.3-canary.17
 - @vercel/frameworks@0.5.1-canary.11
 - @vercel/routing-utils@1.11.4-canary.6
2021-11-08 14:42:18 -06:00
JJ Kasper
0f7e89f76c [routing-utils] Add caseSensitive field to routes schema (#6952)
### Related Issues

x-ref: https://github.com/vercel/customer-issues/issues/34

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2021-11-08 20:29:31 +00:00
Andy Bitz
e68ed33a88 Publish Canary
- vercel@23.1.3-canary.24
 - vercel-plugin-middleware@0.0.0-canary.6
2021-11-08 19:38:58 +01:00
Gary Borton
d3e98cdb73 [cli] Run middleware as a plugin instead of running directly (#6941)
* Move loadPlugins to a utils file to be shared w/ dev server.

* Update loadCliPlugins to also return startDevServer and runDevMiddleware based plugins.

* Move plugins back to dependencies.

These can't be bundled as it interferes with plugin resolution.

* Hook up middleware plugins to dev server.

* Pass output object to loadCliPlugins instead of a logging function.

* Allow more than one runDevMiddleware defining plugins.

* Bundle esbuild w/ middleware plugin.

* Keep esbuild as an external

* Update middleware's esbuild.

* set old space size

* Revert "set old space size"

This reverts commit b579194a862949a11769e9087f01c31f2e1f3b60.

* Use --max-old-space-size for CLI unit tests

* Increase memory

* Use `run.js` to set the memory

* Make NODE_OPTIONS optional

Co-authored-by: Leo Lamprecht <leo@vercel.com>
Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
Co-authored-by: Andy Bitz <artzbitz@gmail.com>
2021-11-08 19:38:13 +01:00
Andy Bitz
bf4e77110f Publish Canary
- vercel@23.1.3-canary.23
 - @vercel/node@1.12.2-canary.6
 - vercel-plugin-node@1.12.2-canary.7
2021-11-08 15:00:01 +01:00
Nathan Rajlich
5b5197d2c5 [node] Create vercel-plugin-node (#6874)
* [node] Refactor to Vercel CLI Plugin

* Enforce "index" suffix on output Serverless Functions

So that nesting works properly

* Some cleanup

* Add version

* Use `@vercel/static-config`

* .

* Add support for wildcard routes

* Don't compile dotfiles, underscore prefixed files, files within `node_modules`, nor TypeScript definition files

Matches the logic from `maybeGetBuilder()` in `@vercel/build-utils`.

* Bump version

* Introduce testing framework

* Debug

* Add test without any deps

* Longer timeout to install Node.js for vercel/fun

* Install deps

* Add legacy Node.js server interface test

* More tests

* Test "assets" fixture

* Test "helpers" fixture

* fix

* Support AWS native API

* Remove debugging `console.log()` calls

* Use plugin-node for new plugin instead

* Revert "Use plugin-node for new plugin instead"

This reverts commit f317b8c6ecdc67a74d5f2b12a2e7567a27d4b6b8.

* Move to `plugin-node` directory

* Update plugin-node version in package.json

* Checkout node from main

* Add yarn.lock files for tests

* Update node-bridge

Co-authored-by: Andy <AndyBitz@users.noreply.github.com>
Co-authored-by: Andy Bitz <artzbitz@gmail.com>
2021-11-08 14:59:01 +01:00
Steven
a6ccf6c180 Publish Canary
- @vercel/build-utils@2.12.3-canary.15
 - vercel@23.1.3-canary.22
 - @vercel/client@10.2.3-canary.16
 - @vercel/node-bridge@2.1.1-canary.2
 - @vercel/node@1.12.2-canary.5
2021-11-05 18:40:08 -04:00
Steven
8d848ebe8b [node] Fix launcher ESM on Windows dev (#6953) 2021-11-05 18:38:55 -04:00
Steven
6ef2c16d63 [build-utils] Add convertRuntimeToPlugin() (#6942) 2021-11-05 16:12:21 -04:00
Andy Bitz
6c71ceaaeb Publish Canary
- vercel@23.1.3-canary.21
2021-11-05 10:26:15 +01:00
Andy
1dcb6dfc6f [cli] Rename certain outputs in vercel build (#6948) 2021-11-05 10:24:49 +01:00
300 changed files with 10617 additions and 1099 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.4.1
- uses: styfle/cancel-workflow-action@0.9.1
with:
workflow_id: 849295, 849296, 849297, 849298
access_token: ${{ github.token }}

View File

@@ -11,7 +11,7 @@ on:
jobs:
test:
name: Dev
timeout-minutes: 60
timeout-minutes: 75
strategy:
fail-fast: false
matrix:

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ test/lib/deployment/failed-page.txt
/public
__pycache__
.vercel
.output

View File

@@ -332,7 +332,7 @@ This is an abstract enumeration type that is implemented by one of the following
- `nodejs10.x`
- `go1.x`
- `java11`
- `python3.8`
- `python3.9`
- `python3.6`
- `dotnetcore2.1`
- `ruby2.5`
@@ -398,12 +398,12 @@ This utility allows you to _scan_ the filesystem and return a [`Files`](#files)
The following trivial example downloads everything to the filesystem, only to return it back (therefore just re-creating the passed-in [`Files`](#files)):
```js
const { glob, download } = require('@vercel/build-utils')
const { glob, download } = require('@vercel/build-utils');
exports.build = ({ files, workPath }) => {
await download(files, workPath)
return glob('**', workPath)
}
await download(files, workPath);
return glob('**', workPath);
};
```
### `getWritableDirectory()`

View File

@@ -14,8 +14,6 @@ const frameworks = (_frameworks as Framework[])
sort: undefined,
dependency: undefined,
defaultRoutes: undefined,
devCommand: undefined,
buildCommand: undefined,
};
if (framework.logo) {

View File

@@ -24,7 +24,7 @@
"eslint-config-prettier": "8.3.0",
"eslint-plugin-jest": "24.3.6",
"husky": "6.0.0",
"jest": "27.0.6",
"jest": "27.3.1",
"json5": "2.1.1",
"lint-staged": "9.2.5",
"node-fetch": "2.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.12.3-canary.14",
"version": "2.12.3-canary.20",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -21,7 +21,7 @@
"@types/async-retry": "^1.2.1",
"@types/cross-spawn": "6.0.0",
"@types/end-of-stream": "^1.4.0",
"@types/fs-extra": "^5.0.5",
"@types/fs-extra": "9.0.13",
"@types/glob": "^7.1.1",
"@types/jest": "27.0.1",
"@types/js-yaml": "3.12.1",
@@ -30,7 +30,7 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.5.1-canary.10",
"@vercel/frameworks": "0.5.1-canary.12",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",
@@ -38,7 +38,7 @@
"boxen": "4.2.0",
"cross-spawn": "6.0.5",
"end-of-stream": "1.4.1",
"fs-extra": "7.0.0",
"fs-extra": "10.0.0",
"glob": "7.1.3",
"into-stream": "5.0.0",
"js-yaml": "3.13.1",

View File

@@ -0,0 +1,202 @@
import fs from 'fs-extra';
import { join, dirname, relative } from 'path';
import glob from './fs/glob';
import { normalizePath } from './fs/normalize-path';
import { FILES_SYMBOL, getLambdaOptionsFromFunction, Lambda } from './lambda';
import type FileBlob from './file-blob';
import type { BuilderFunctions, BuildOptions, Files } from './types';
import minimatch from 'minimatch';
/**
* Convert legacy Runtime to a Plugin.
* @param buildRuntime - a legacy build() function from a Runtime
* @param ext - the file extension, for example `.py`
*/
export function convertRuntimeToPlugin(
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
ext: string
) {
return async function build({ workPath }: { workPath: string }) {
const opts = { cwd: workPath };
const files = await glob('**', opts);
delete files['vercel.json']; // Builders/Runtimes didn't have vercel.json
const entrypoints = await glob(`api/**/*${ext}`, opts);
const pages: { [key: string]: any } = {};
const { functions = {} } = await readVercelConfig(workPath);
const traceDir = join(workPath, '.output', 'runtime-traced-files');
await fs.ensureDir(traceDir);
for (const entrypoint of Object.keys(entrypoints)) {
const key =
Object.keys(functions).find(
src => src === entrypoint || minimatch(entrypoint, src)
) || '';
const config = functions[key] || {};
const { output } = await buildRuntime({
files,
entrypoint,
workPath,
config: {
zeroConfig: true,
includeFiles: config.includeFiles,
excludeFiles: config.excludeFiles,
},
});
pages[entrypoint] = {
handler: output.handler,
runtime: output.runtime,
memory: output.memory,
maxDuration: output.maxDuration,
environment: output.environment,
allowQuery: output.allowQuery,
regions: output.regions,
};
// @ts-ignore This symbol is a private API
const lambdaFiles: Files = output[FILES_SYMBOL];
const entry = join(workPath, '.output', 'server', 'pages', entrypoint);
await fs.ensureDir(dirname(entry));
await linkOrCopy(files[entrypoint].fsPath, entry);
const tracedFiles: {
absolutePath: string;
relativePath: string;
}[] = [];
Object.entries(lambdaFiles).forEach(async ([relPath, file]) => {
const newPath = join(traceDir, relPath);
tracedFiles.push({ absolutePath: newPath, relativePath: relPath });
if (file.fsPath) {
await linkOrCopy(file.fsPath, newPath);
} else if (file.type === 'FileBlob') {
const { data, mode } = file as FileBlob;
await fs.writeFile(newPath, data, { mode });
} else {
throw new Error(`Unknown file type: ${file.type}`);
}
});
const nft = join(
workPath,
'.output',
'server',
'pages',
`${entrypoint}.nft.json`
);
const json = JSON.stringify({
version: 1,
files: tracedFiles.map(f => ({
input: normalizePath(relative(nft, f.absolutePath)),
output: normalizePath(f.relativePath),
})),
});
await fs.ensureDir(dirname(nft));
await fs.writeFile(nft, json);
}
await updateFunctionsManifest({ workPath, pages });
};
}
async function linkOrCopy(existingPath: string, newPath: string) {
try {
await fs.createLink(existingPath, newPath);
} catch (err: any) {
if (err.code !== 'EEXIST') {
await fs.copyFile(existingPath, newPath);
}
}
}
async function readJson(filePath: string): Promise<{ [key: string]: any }> {
try {
const str = await fs.readFile(filePath, 'utf8');
return JSON.parse(str);
} catch (err) {
if (err.code === 'ENOENT') {
return {};
}
throw err;
}
}
async function readVercelConfig(
workPath: string
): Promise<{ functions?: BuilderFunctions; regions?: string[] }> {
const vercelJsonPath = join(workPath, 'vercel.json');
return readJson(vercelJsonPath);
}
/**
* If `.output/functions-manifest.json` exists, append to the pages
* property. Otherwise write a new file. This will also read `vercel.json`
* and apply relevant `functions` property config.
*/
export async function updateFunctionsManifest({
workPath,
pages,
}: {
workPath: string;
pages: { [key: string]: any };
}) {
const functionsManifestPath = join(
workPath,
'.output',
'functions-manifest.json'
);
const vercelConfig = await readVercelConfig(workPath);
const functionsManifest = await readJson(functionsManifestPath);
if (!functionsManifest.version) functionsManifest.version = 1;
if (!functionsManifest.pages) functionsManifest.pages = {};
for (const [pageKey, pageConfig] of Object.entries(pages)) {
const fnConfig = await getLambdaOptionsFromFunction({
sourceFile: pageKey,
config: vercelConfig,
});
functionsManifest.pages[pageKey] = {
...pageConfig,
memory: fnConfig.memory || pageConfig.memory,
maxDuration: fnConfig.maxDuration || pageConfig.maxDuration,
regions: vercelConfig.regions || pageConfig.regions,
};
}
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
}
/**
* Will append routes to the `routes-manifest.json` file.
* If the file does not exist, it'll be created.
*/
export async function updateRoutesManifest({
workPath,
dynamicRoutes,
}: {
workPath: string;
dynamicRoutes?: {
page: string;
regex: string;
namedRegex?: string;
routeKeys?: { [named: string]: string };
}[];
}) {
const routesManifestPath = join(workPath, '.output', 'routes-manifest.json');
const routesManifest = await readJson(routesManifestPath);
if (!routesManifest.version) routesManifest.version = 1;
if (routesManifest.pages404 === undefined) routesManifest.pages404 = true;
if (dynamicRoutes) {
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(...dynamicRoutes);
}
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
}

View File

@@ -3,6 +3,7 @@ import assert from 'assert';
import vanillaGlob_ from 'glob';
import { promisify } from 'util';
import { lstat, Stats } from 'fs-extra';
import { normalizePath } from './normalize-path';
import FileFsRef from '../file-fs-ref';
export type GlobOptions = vanillaGlob_.IOptions;
@@ -45,7 +46,7 @@ export default async function glob(
const files = await vanillaGlob(pattern, options);
for (const relativePath of files) {
const fsPath = path.join(options.cwd!, relativePath).replace(/\\/g, '/');
const fsPath = normalizePath(path.join(options.cwd!, relativePath));
let stat: Stats = options.statCache![fsPath] as Stats;
assert(
stat,

View File

@@ -0,0 +1,8 @@
const isWin = process.platform === 'win32';
/**
* Convert Windows separators to Unix separators.
*/
export function normalizePath(p: string): string {
return isWin ? p.replace(/\\/g, '/') : p;
}

View File

@@ -81,6 +81,12 @@ export {
export { detectFramework } from './detect-framework';
export { DetectorFilesystem } from './detectors/filesystem';
export { readConfigFile } from './fs/read-config-file';
export { normalizePath } from './fs/normalize-path';
export {
convertRuntimeToPlugin,
updateFunctionsManifest,
updateRoutesManifest,
} from './convert-runtime-to-plugin';
export * from './schemas';
export * from './types';

View File

@@ -36,9 +36,11 @@ interface CreateLambdaOptions {
interface GetLambdaOptionsFromFunctionOptions {
sourceFile: string;
config?: Config;
config?: Pick<Config, 'functions'>;
}
export const FILES_SYMBOL = Symbol('files');
export class Lambda {
public type: 'Lambda';
public zipBuffer: Buffer;
@@ -118,7 +120,7 @@ export async function createLambda({
try {
const zipBuffer = await createZip(files);
return new Lambda({
const lambda = new Lambda({
zipBuffer,
handler,
runtime,
@@ -127,6 +129,9 @@ export async function createLambda({
environment,
regions,
});
// @ts-ignore This symbol is a private API
lambda[FILES_SYMBOL] = files;
return lambda;
} finally {
sema.release();
}

View File

@@ -10,6 +10,7 @@ export interface File {
mode: number;
contentType?: string;
toStream: () => NodeJS.ReadableStream;
toStreamAsync?: () => Promise<NodeJS.ReadableStream>;
/**
* The absolute path to the file in the filesystem
*/

View File

@@ -1,5 +1,5 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@vercel/static-build" }],
"probes": [{ "path": "/", "mustContain": "npm version: 7" }]
"probes": [{ "path": "/", "mustContain": "npm version: 8" }]
}

View File

@@ -0,0 +1,182 @@
import { join } from 'path';
import fs from 'fs-extra';
import { BuildOptions, createLambda } from '../src';
import { convertRuntimeToPlugin } from '../src/convert-runtime-to-plugin';
async function fsToJson(dir: string, output: Record<string, any> = {}) {
const files = await fs.readdir(dir);
for (const file of files) {
const fsPath = join(dir, file);
const stat = await fs.stat(fsPath);
if (stat.isDirectory()) {
output[file] = {};
await fsToJson(fsPath, output[file]);
} else {
output[file] = await fs.readFile(fsPath, 'utf8');
}
}
return output;
}
const workPath = join(__dirname, 'walk', 'python-api');
describe('convert-runtime-to-plugin', () => {
afterEach(async () => {
await fs.remove(join(workPath, '.output'));
});
it('should create correct fileystem for python', async () => {
const lambdaOptions = {
handler: 'index.handler',
runtime: 'python3.9',
memory: 512,
maxDuration: 5,
environment: {},
regions: ['sfo1'],
};
const buildRuntime = async (opts: BuildOptions) => {
const lambda = await createLambda({
files: opts.files,
...lambdaOptions,
});
return { output: lambda };
};
const lambdaFiles = await fsToJson(workPath);
delete lambdaFiles['vercel.json'];
const build = await convertRuntimeToPlugin(buildRuntime, '.py');
await build({ workPath });
const output = await fsToJson(join(workPath, '.output'));
expect(output).toMatchObject({
'functions-manifest.json': expect.stringContaining('{'),
'runtime-traced-files': lambdaFiles,
server: {
pages: {
api: {
'index.py': expect.stringContaining('index'),
'index.py.nft.json': expect.stringContaining('{'),
users: {
'get.py': expect.stringContaining('get'),
'get.py.nft.json': expect.stringContaining('{'),
'post.py': expect.stringContaining('post'),
'post.py.nft.json': expect.stringContaining('{'),
},
},
},
},
});
const funcManifest = JSON.parse(output['functions-manifest.json']);
expect(funcManifest).toMatchObject({
version: 1,
pages: {
'api/index.py': lambdaOptions,
'api/users/get.py': lambdaOptions,
'api/users/post.py': { ...lambdaOptions, memory: 3008 },
},
});
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
expect(indexJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input: '../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',
},
{
input: '../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
],
});
const getJson = JSON.parse(
output.server.pages.api.users['get.py.nft.json']
);
expect(getJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',
},
{
input: '../../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
],
});
const postJson = JSON.parse(
output.server.pages.api.users['post.py.nft.json']
);
expect(postJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',
},
{
input: '../../../../../runtime-traced-files/api/users/post.py',
output: 'api/users/post.py',
},
{
input: '../../../../../runtime-traced-files/file.txt',
output: 'file.txt',
},
{
input: '../../../../../runtime-traced-files/util/date.py',
output: 'util/date.py',
},
{
input: '../../../../../runtime-traced-files/util/math.py',
output: 'util/math.py',
},
],
});
expect(output.server.pages['file.txt']).toBeUndefined();
expect(output.server.pages.api['file.txt']).toBeUndefined();
});
});

View File

@@ -0,0 +1 @@
# index

View File

@@ -0,0 +1 @@
# get

View File

@@ -0,0 +1 @@
# post

View File

@@ -0,0 +1 @@
This file should also be included

View File

@@ -0,0 +1 @@
# date

View File

@@ -0,0 +1 @@
# math

View File

@@ -0,0 +1,10 @@
{
"functions": {
"api/users/post.py": {
"memory": 3008
},
"api/not-matching-anything.py": {
"memory": 768
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "23.1.3-canary.20",
"version": "23.1.3-canary.38",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -43,12 +43,14 @@
"node": ">= 12"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.14",
"@vercel/go": "1.2.4-canary.3",
"@vercel/node": "1.12.2-canary.4",
"@vercel/python": "2.0.6-canary.4",
"@vercel/ruby": "1.2.8-canary.3",
"update-notifier": "4.1.0"
"@vercel/build-utils": "2.12.3-canary.20",
"@vercel/go": "1.2.4-canary.4",
"@vercel/node": "1.12.2-canary.7",
"@vercel/python": "2.1.1",
"@vercel/ruby": "1.2.8-canary.4",
"update-notifier": "4.1.0",
"vercel-plugin-middleware": "0.0.0-canary.7",
"vercel-plugin-node": "1.12.2-canary.11"
},
"devDependencies": {
"@next/env": "11.1.2",
@@ -88,7 +90,7 @@
"@types/update-notifier": "5.1.0",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.5.1-canary.10",
"@vercel/frameworks": "0.5.1-canary.12",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.17.0",
"@zeit/fun": "0.11.2",
@@ -164,8 +166,6 @@
"typescript": "4.3.4",
"universal-analytics": "0.4.20",
"utility-types": "2.1.0",
"vercel-plugin-middleware": "0.0.0-canary.5",
"vercel-plugin-node": "1.12.2-plugin.6",
"which": "2.0.2",
"write-json-file": "2.2.0",
"xdg-app-paths": "5.1.0"

View File

@@ -23,19 +23,14 @@ import handleError from '../util/handle-error';
import confirm from '../util/input/confirm';
import { isSettingValue } from '../util/is-setting-value';
import cmd from '../util/output/cmd';
import code from '../util/output/code';
import { getColorForPkgName } from '../util/output/color-name-cache';
import logo from '../util/output/logo';
import param from '../util/output/param';
import stamp from '../util/output/stamp';
import cliPkgJson from '../util/pkg';
import { getCommandName, getPkgName } from '../util/pkg-name';
import { loadCliPlugins } from '../util/plugins';
import { findFramework } from '../util/projects/find-framework';
import { VERCEL_DIR } from '../util/projects/link';
import {
ProjectLinkAndSettings,
readProjectSettings,
} from '../util/projects/project-settings';
import { readProjectSettings } from '../util/projects/project-settings';
import pull from './pull';
const sema = new Sema(16, {
@@ -69,18 +64,24 @@ const help = () => {
};
const OUTPUT_DIR = '.output';
const VERCEL_PLUGIN_PREFIX = 'vercel-plugin-';
const fields: {
name: string;
value: keyof ProjectLinkAndSettings['settings'];
}[] = [
{ name: 'Build Command', value: 'buildCommand' },
{ name: 'Output Directory', value: 'outputDirectory' },
{ name: 'Root Directory', value: 'rootDirectory' },
];
export default async function main(client: Client) {
if (process.env.__VERCEL_BUILD_RUNNING) {
client.output.error(
`${cmd(
`${getPkgName()} build`
)} must not recursively invoke itself. Check the Build Command in the Project Settings or the ${cmd(
'build'
)} script in ${cmd('package.json')}`
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
return 1;
} else {
process.env.__VERCEL_BUILD_RUNNING = '1';
}
let argv;
const buildStamp = stamp();
try {
@@ -120,6 +121,9 @@ export default async function main(client: Client) {
project = await readProjectSettings(join(cwd, VERCEL_DIR));
}
// If `rootDirectory` exists, then `baseDir` will be the repo's root directory.
const baseDir = cwd;
cwd = project.settings.rootDirectory
? join(cwd, project.settings.rootDirectory)
: cwd;
@@ -152,47 +156,57 @@ export default async function main(client: Client) {
}
const buildState = { ...project.settings };
client.output.log(`Retrieved Project Settings:`);
client.output.print(
chalk.dim(` - ${chalk.bold(`Framework Preset:`)} ${framework.name}\n`)
const formatSetting = (
name: string,
override: string | null | undefined,
defaults: typeof framework.settings.outputDirectory
) =>
` - ${chalk.bold(`${name}:`)} ${`${
override
? override + ` (override)`
: 'placeholder' in defaults
? chalk.italic(`${defaults.placeholder}`)
: defaults.value
}`}`;
console.log(`Retrieved Project Settings:`);
console.log(
chalk.dim(` - ${chalk.bold(`Framework Preset:`)} ${framework.name}`)
);
for (let field of fields) {
const defaults = (framework.settings as any)[field.value];
if (defaults) {
client.output.print(
console.log(
chalk.dim(
` - ${chalk.bold(`${field.name}:`)} ${`${
project.settings[field.value]
? project.settings[field.value] + ` (override)`
: isSettingValue(defaults)
? defaults.value
: chalk.italic(`${defaults.placeholder}`)
}`}\n`
formatSetting(
'Build Command',
project.settings.buildCommand,
framework.settings.buildCommand
)
)
);
}
if (field.value != 'buildCommand') {
(buildState as any)[field.value] = project.settings[field.value]
? project.settings[field.value]
: defaults
? isSettingValue(defaults)
? defaults.value
: null
: null;
}
}
console.log(
chalk.dim(
formatSetting(
'Output Directory',
project.settings.outputDirectory,
framework.settings.outputDirectory
)
)
);
buildState.outputDirectory =
project.settings.outputDirectory ||
(isSettingValue(framework.settings.outputDirectory)
? framework.settings.outputDirectory.value
: null);
buildState.rootDirectory = project.settings.rootDirectory;
if (loadedEnvFiles.length > 0) {
client.output.log(
console.log(
`Loaded Environment Variables from ${loadedEnvFiles.length} ${pluralize(
'file',
loadedEnvFiles.length
)}:`
);
for (let envFile of loadedEnvFiles) {
client.output.print(chalk.dim(` - ${envFile.path}\n`));
console.log(chalk.dim(` - ${envFile.path}`));
}
}
@@ -200,7 +214,7 @@ export default async function main(client: Client) {
const debug = argv['--debug'];
let plugins;
try {
plugins = await loadCliPlugins(client, cwd);
plugins = await loadCliPlugins(cwd, client.output);
} catch (error) {
client.output.error('Failed to load CLI Plugins');
handleError(error, { debug });
@@ -223,7 +237,7 @@ export default async function main(client: Client) {
};
if (plugins?.pluginCount && plugins?.pluginCount > 0) {
client.output.log(
console.log(
`Loaded ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
plugins.pluginCount
@@ -231,7 +245,7 @@ export default async function main(client: Client) {
);
// preBuild Plugins
if (plugins.preBuildPlugins.length > 0) {
client.output.log(
console.log(
`Running ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
plugins.pluginCount
@@ -269,17 +283,19 @@ export default async function main(client: Client) {
// Clean the output directory
fs.removeSync(join(cwd, OUTPUT_DIR));
if (typeof buildState.buildCommand === 'string') {
client.output.log(`Running Build Command: ${cmd(buildState.buildCommand)}`);
await execCommand(buildState.buildCommand, {
...spawnOpts,
// Yarn v2 PnP mode may be activated, so force
// "node-modules" linker style
env: {
const env = {
YARN_NODE_LINKER: 'node-modules',
...spawnOpts.env,
},
cwd: cwd,
};
if (typeof buildState.buildCommand === 'string') {
console.log(`Running Build Command: ${cmd(buildState.buildCommand)}`);
await execCommand(buildState.buildCommand, {
...spawnOpts,
env,
cwd,
});
} else if (fs.existsSync(join(cwd, 'package.json'))) {
await runPackageJsonScript(
@@ -288,6 +304,15 @@ export default async function main(client: Client) {
['vercel-build', 'now-build', 'build'],
spawnOpts
);
} else if (typeof framework.settings.buildCommand.value === 'string') {
console.log(
`Running Build Command: ${cmd(framework.settings.buildCommand.value)}`
);
await execCommand(framework.settings.buildCommand.value, {
...spawnOpts,
env,
cwd,
});
}
if (!fs.existsSync(join(cwd, OUTPUT_DIR))) {
@@ -317,6 +342,7 @@ export default async function main(client: Client) {
'_middleware.js',
'api/**',
'.git/**',
'.next/cache/**',
],
nodir: true,
dot: true,
@@ -335,7 +361,7 @@ export default async function main(client: Client) {
)
);
client.output.stopSpinner();
client.output.log(
console.log(
`Copied ${files.length.toLocaleString()} files from ${param(
distDir
)} to ${param(outputDir)} ${copyStamp()}`
@@ -389,6 +415,36 @@ export default async function main(client: Client) {
join(cwd, OUTPUT_DIR, 'static', '_next', 'static')
);
// Next.js might reference files from the `static` directory in `middleware-manifest.json`.
// Since we move all files from `static` to `static/_next/static`, we'll need to change
// those references as well and update the manifest file.
const middlewareManifest = join(
cwd,
OUTPUT_DIR,
'server',
'middleware-manifest.json'
);
if (fs.existsSync(middlewareManifest)) {
const manifest = await fs.readJSON(middlewareManifest);
Object.keys(manifest.middleware).forEach(key => {
const files = manifest.middleware[key].files.map((f: string) => {
if (f.startsWith('static/')) {
const next = f.replace(/^static\//gm, 'static/_next/static/');
client.output.debug(
`Replacing file in \`middleware-manifest.json\`: ${f} => ${next}`
);
return next;
}
return f;
});
manifest.middleware[key].files = files;
});
await fs.writeJSON(middlewareManifest, manifest);
}
// We want to pick up directories for user-provided static files into `.`output/static`.
// More specifically, the static directory contents would then be mounted to `output/static/static`,
// and the public directory contents would be mounted to `output/static`. Old Next.js versions
@@ -477,7 +533,7 @@ export default async function main(client: Client) {
fileList.delete(relative(cwd, f));
await resolveNftToOutput({
client,
cwd,
baseDir,
outputDir: OUTPUT_DIR,
nftFileName: f.replace(ext, '.js.nft.json'),
nft: {
@@ -493,7 +549,7 @@ export default async function main(client: Client) {
const json = await fs.readJson(f);
await resolveNftToOutput({
client,
cwd,
baseDir,
outputDir: OUTPUT_DIR,
nftFileName: f,
nft: json,
@@ -511,17 +567,22 @@ export default async function main(client: Client) {
await fs.writeJSON(requiredServerFilesPath, {
...requiredServerFilesJson,
appDir: '.',
files: requiredServerFilesJson.files.map((i: string) => ({
files: requiredServerFilesJson.files.map((i: string) => {
const absolutePath = join(cwd, i.replace('.next', '.output'));
const output = relative(baseDir, absolutePath);
return {
input: i.replace('.next', '.output'),
output: i,
})),
output,
};
}),
});
}
}
// Build Plugins
if (plugins?.buildPlugins && plugins.buildPlugins.length > 0) {
client.output.log(
console.log(
`Running ${plugins.pluginCount} CLI ${pluralize(
'Plugin',
plugins.pluginCount
@@ -556,13 +617,13 @@ export default async function main(client: Client) {
}
}
client.output.print(
console.log(
`${prependEmoji(
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
buildStamp()
)}`,
emoji('success')
)}\n`
)}`
);
return 0;
@@ -608,71 +669,37 @@ export async function runPackageJsonScript(
}
}
client.output.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
console.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
await spawnAsync(cliType, ['run', scriptName], opts);
client.output.print('\n'); // give it some room
console.log(); // give it some room
client.output.debug(`Script complete [${Date.now() - runScriptTime}ms]`);
return true;
}
async function loadCliPlugins(client: Client, cwd: string) {
const { packageJson } = await scanParentDirs(cwd, true);
let pluginCount = 0;
const preBuildPlugins = [];
const buildPlugins = [];
const deps = new Set(
[
...Object.keys(packageJson?.dependencies || {}),
...Object.keys(packageJson?.devDependencies || {}),
...Object.keys(cliPkgJson.dependencies),
].filter(dep => dep.startsWith(VERCEL_PLUGIN_PREFIX))
);
for (let dep of deps) {
pluginCount++;
const resolved = require.resolve(dep, {
paths: [cwd, process.cwd(), __dirname],
});
let plugin;
try {
plugin = require(resolved);
const color = getColorForPkgName(dep);
if (typeof plugin.preBuild === 'function') {
preBuildPlugins.push({
plugin,
name: dep,
color,
});
}
if (typeof plugin.build === 'function') {
buildPlugins.push({
plugin,
name: dep,
color,
});
}
} catch (error) {
client.output.error(`Failed to import ${code(dep)}`);
throw error;
}
}
return { pluginCount, preBuildPlugins, buildPlugins };
}
async function linkOrCopy(existingPath: string, newPath: string) {
try {
if (
newPath.endsWith('.nft.json') ||
newPath.endsWith('middleware-manifest.json') ||
newPath.endsWith('required-server-files.json')
) {
await fs.copy(existingPath, newPath, {
overwrite: true,
});
} else {
await fs.createLink(existingPath, newPath);
}
} catch (err: any) {
// eslint-disable-line
// If a hard link to the same file already exists
// If a symlink to the same file already exists
// then trying to copy it will make an empty file from it.
if (err['code'] === 'EEXIST') return;
// In some VERY rare cases (1 in a thousand), hard-link creation fails on Windows.
// In some VERY rare cases (1 in a thousand), symlink creation fails on Windows.
// In that case, we just fall back to copying.
// This issue is reproducible with "pnpm add @material-ui/icons@4.9.1"
await fs.copyFile(existingPath, newPath);
await fs.copy(existingPath, newPath, {
overwrite: true,
});
}
}
@@ -716,13 +743,13 @@ interface NftFile {
// properly with `vc --prebuilt`.
async function resolveNftToOutput({
client,
cwd,
baseDir,
outputDir,
nftFileName,
nft,
}: {
client: Client;
cwd: string;
baseDir: string;
outputDir: string;
nftFileName: string;
nft: NftFile;
@@ -742,9 +769,15 @@ async function resolveNftToOutput({
const newFilePath = join(outputDir, 'inputs', hash(raw) + ext);
smartCopy(client, fullInput, newFilePath);
// We have to use `baseDir` instead of `cwd`, because we want to
// mount everything from there (especially `node_modules`).
// This is important for NPM Workspaces where `node_modules` is not
// in the directory of the workspace.
const output = relative(baseDir, fullInput).replace('.output', '.next');
newFilesList.push({
input: relative(parse(nftFileName).dir, newFilePath),
output: relative(cwd, fullInput).replace('.output', '.next'),
output,
});
} else {
newFilesList.push(relativeInput);

View File

@@ -6,7 +6,6 @@ import { ProjectEnvVariable } from '../../types';
import Client from '../../util/client';
import { getLinkedProject } from '../../util/projects/link';
import { getFrameworks } from '../../util/get-frameworks';
import { isSettingValue } from '../../util/is-setting-value';
import { ProjectSettings } from '../../types';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
import setupAndLink from '../../util/link/setup-and-link';
@@ -71,9 +70,9 @@ export default async function dev(
frameworkSlug = framework.slug;
}
const defaults = framework.settings.devCommand;
if (isSettingValue(defaults)) {
devCommand = defaults.value;
const defaults = framework.settings.devCommand.value;
if (defaults) {
devCommand = defaults;
}
}
}

View File

@@ -48,6 +48,22 @@ const help = () => {
};
export default async function main(client: Client) {
if (process.env.__VERCEL_DEV_RUNNING) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
);
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
return 1;
} else {
process.env.__VERCEL_DEV_RUNNING = '1';
}
let argv;
let args;
const { output } = client;
@@ -90,22 +106,21 @@ export default async function main(client: Client) {
if (pkg) {
const { scripts } = pkg as PackageJson;
if (scripts && scripts.dev && /\bnow\b\W+\bdev\b/.test(scripts.dev)) {
output.error(
`The ${cmd('dev')} script in ${cmd(
'package.json'
)} must not contain ${cmd('now dev')}`
if (
scripts &&
scripts.dev &&
/\b(now|vercel)\b\W+\bdev\b/.test(scripts.dev)
) {
client.output.error(
`${cmd(
`${getPkgName()} dev`
)} must not recursively invoke itself. Check the Development Command in the Project Settings or the ${cmd(
'dev'
)} script in ${cmd('package.json')}`
);
output.error(`Learn More: http://err.sh/vercel/now-dev-as-dev-script`);
return 1;
}
if (scripts && scripts.dev && /\bvercel\b\W+\bdev\b/.test(scripts.dev)) {
output.error(
`The ${cmd('dev')} script in ${cmd(
'package.json'
)} must not contain ${cmd('vercel dev')}`
client.output.error(
`Learn More: https://vercel.link/recursive-invocation-of-commands`
);
output.error(`Learn More: http://err.sh/vercel/now-dev-as-dev-script`);
return 1;
}
}

View File

@@ -160,24 +160,26 @@ const main = async () => {
// * a path to deploy (as in: `vercel path/`)
// * a subcommand (as in: `vercel ls`)
const targetOrSubcommand = argv._[2];
const isBuildOrDev =
targetOrSubcommand === 'build' || targetOrSubcommand === 'dev';
if (isBuildOrDev) {
console.log(
`${chalk.grey(
`${getTitleName()} CLI ${
pkg.version
} ${targetOrSubcommand} (beta) — https://vercel.com/feedback`
)}`
);
} else {
output.print(
`${chalk.grey(
`${getTitleName()} CLI ${pkg.version}${
targetOrSubcommand === 'dev'
? ' dev (beta)'
: targetOrSubcommand === 'build'
? ' build (beta)'
: ''
}${
isCanary ||
targetOrSubcommand === 'dev' ||
targetOrSubcommand === 'build'
? ' — https://vercel.com/feedback'
: ''
isCanary ? ' — https://vercel.com/feedback' : ''
}`
)}\n`
);
}
// Handle `--version` directly
if (!targetOrSubcommand && argv['--version']) {

View File

@@ -18,12 +18,8 @@ export const isDirectory = (path: string): boolean => {
const getGlobalPathConfig = (): string => {
let customPath: string | undefined;
try {
const argv = getArgs(process.argv.slice(2), {});
const argv = getArgs(process.argv.slice(2), {}, { permissive: true });
customPath = argv['--global-config'];
} catch (_error) {
// args are optional so consume error
}
const vercelDirectories = XDGAppPaths('com.vercel.cli').dataDirs();

View File

@@ -7,12 +7,8 @@ import getArgs from '../../util/get-args';
export default function getLocalPathConfig(prefix: string) {
let customPath: string | undefined;
try {
const argv = getArgs(process.argv.slice(2), {});
const argv = getArgs(process.argv.slice(2), {}, { permissive: true });
customPath = argv['--local-config'];
} catch (_error) {
// args are optional so consume error
}
// If `--local-config` flag was specified, then that takes priority
if (customPath) {

View File

@@ -22,8 +22,6 @@ import deepEqual from 'fast-deep-equal';
import which from 'which';
import npa from 'npm-package-arg';
import { runDevMiddleware } from 'vercel-plugin-middleware';
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
import {
getTransformedRoutes,
@@ -91,6 +89,7 @@ import {
} from './types';
import { ProjectEnvVariable, ProjectSettings } from '../../types';
import exposeSystemEnvs from './expose-system-envs';
import { loadCliPlugins } from '../plugins';
const frontendRuntimeSet = new Set(
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
@@ -1351,6 +1350,30 @@ export default class DevServer {
return false;
};
runDevMiddleware = async (
req: http.IncomingMessage,
res: http.ServerResponse
) => {
const { devMiddlewarePlugins } = await loadCliPlugins(
this.cwd,
this.output
);
try {
for (let plugin of devMiddlewarePlugins) {
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
if (result.finished) {
return result;
}
}
return { finished: false };
} catch (e) {
return {
finished: true,
error: e,
};
}
};
/**
* Serve project directory as a v2 deployment.
*/
@@ -1418,7 +1441,7 @@ export default class DevServer {
let prevUrl = req.url;
let prevHeaders: HttpHeadersConfig = {};
const middlewareResult = await runDevMiddleware(req, res, this.cwd);
const middlewareResult = await this.runDevMiddleware(req, res);
if (middlewareResult) {
if (middlewareResult.error) {
@@ -2138,7 +2161,10 @@ export default class DevServer {
process.stdout.write(data.replace(proxyPort, devPort));
});
p.on('exit', () => {
p.on('exit', (code: number) => {
if (code > 0) {
process.exit(code);
}
this.devProcessPort = undefined;
});

View File

@@ -1,11 +1,9 @@
import { Stats } from 'fs';
import { sep, dirname, join, resolve } from 'path';
import { readJSON, lstat, readlink, readFile, realpath } from 'fs-extra';
import { lstat, readlink, readFile, realpath } from 'fs-extra';
import { isCanary } from './is-canary';
import { getPkgName } from './pkg-name';
// `npm` tacks a bunch of extra properties on the `package.json` file,
// so check for one of them to determine yarn vs. npm.
async function isYarn(): Promise<boolean> {
let s: Stats;
let binPath = process.argv[1];
@@ -20,8 +18,12 @@ async function isYarn(): Promise<boolean> {
}
}
const pkgPath = join(dirname(binPath), '..', 'package.json');
const pkg = await readJSON(pkgPath).catch(() => ({}));
return !('_id' in pkg);
/*
* Generally, pkgPath looks like:
* "/Users/username/.config/yarn/global/node_modules/vercel/package.json"
* "/usr/local/share/.config/yarn/global/node_modules/vercel/package.json"
*/
return pkgPath.includes(join('yarn', 'global'));
}
async function getConfigPrefix() {

View File

@@ -1,6 +1,5 @@
import crypto from 'crypto';
import ua from 'universal-analytics';
import { platform, release, userInfo } from 'os';
import { getPlatformEnv } from '@vercel/build-utils';
import userAgent from './ua-browser';
@@ -16,10 +15,15 @@ export const shouldCollectMetrics =
export const metrics = (): ua.Visitor => {
const token =
typeof config.token === 'string' ? config.token : platform() + release();
const salt = userInfo().username;
typeof config.token === 'string'
? config.token
: process.platform + process.arch;
const salt =
(process.env.USER || '') +
(process.env.LANG || '') +
(process.env.SHELL || '');
const hash = crypto
.pbkdf2Sync(token, salt, 1000, 64, 'sha512')
.pbkdf2Sync(token, salt, 100, 64, 'sha512')
.toString('hex')
.substring(0, 24);

View File

@@ -125,11 +125,15 @@ export class Output {
this.debug(`Spinner invoked (${message}) with a ${delay}ms delay`);
return;
}
if (this.isTTY) {
if (this._spinner) {
this._spinner.text = message;
} else {
this._spinner = wait(message, delay);
}
} else {
this.print(`${message}\n`);
}
};
stopSpinner = () => {

View File

@@ -1,11 +1,6 @@
import { relative as nativeRelative } from 'path';
const isWin = process.platform === 'win32';
import { normalizePath } from '@vercel/build-utils';
export function relative(a: string, b: string): string {
let p = nativeRelative(a, b);
if (isWin) {
p = p.replace(/\\/g, '/');
}
return p;
return normalizePath(nativeRelative(a, b));
}

View File

@@ -0,0 +1,76 @@
import code from '../util/output/code';
import { getColorForPkgName } from '../util/output/color-name-cache';
import cliPkgJson from '../util/pkg';
import { scanParentDirs } from '@vercel/build-utils';
import { Output } from './output';
const VERCEL_PLUGIN_PREFIX = 'vercel-plugin-';
export async function loadCliPlugins(cwd: string, output: Output) {
const { packageJson } = await scanParentDirs(cwd, true);
let pluginCount = 0;
const preBuildPlugins = [];
const buildPlugins = [];
const devServerPlugins = [];
const devMiddlewarePlugins = [];
const deps = new Set(
[
...Object.keys(packageJson?.dependencies || {}),
...Object.keys(packageJson?.devDependencies || {}),
...Object.keys(cliPkgJson.dependencies),
].filter(dep => dep.startsWith(VERCEL_PLUGIN_PREFIX))
);
for (let dep of deps) {
pluginCount++;
const resolved = require.resolve(dep, {
paths: [cwd, process.cwd(), __dirname],
});
let plugin;
try {
plugin = require(resolved);
const color = getColorForPkgName(dep);
if (typeof plugin.preBuild === 'function') {
preBuildPlugins.push({
plugin,
name: dep,
color,
});
}
if (typeof plugin.build === 'function') {
buildPlugins.push({
plugin,
name: dep,
color,
});
}
if (typeof plugin.startDevServer === 'function') {
devServerPlugins.push({
plugin,
name: dep,
color,
});
}
if (typeof plugin.runDevMiddleware === 'function') {
devMiddlewarePlugins.push({
plugin,
name: dep,
color,
});
}
} catch (error) {
output.error(`Failed to import ${code(dep)}`);
throw error;
}
}
return {
pluginCount,
preBuildPlugins,
buildPlugins,
devServerPlugins,
devMiddlewarePlugins,
};
}

View File

@@ -244,20 +244,27 @@ export async function linkFolderToProject(
try {
const gitIgnorePath = join(path, '.gitignore');
const gitIgnore = await readFile(gitIgnorePath, 'utf8').catch(() => null);
const EOL = gitIgnore && gitIgnore.includes('\r\n') ? '\r\n' : os.EOL;
let gitIgnore =
(await readFile(gitIgnorePath, 'utf8').catch(() => null)) ?? '';
const EOL = gitIgnore.includes('\r\n') ? '\r\n' : os.EOL;
let contentModified = false;
if (
!gitIgnore ||
!gitIgnore.split(EOL).includes(VERCEL_DIR) ||
!gitIgnore.split(EOL).includes(VERCEL_OUTPUT_DIR)
) {
await writeFile(
gitIgnorePath,
gitIgnore
? `${gitIgnore}${EOL}${VERCEL_DIR}${EOL}${VERCEL_OUTPUT_DIR}${EOL}`
: `${VERCEL_DIR}${EOL}${VERCEL_OUTPUT_DIR}${EOL}`
);
if (!gitIgnore.split(EOL).includes(VERCEL_DIR)) {
gitIgnore += `${
gitIgnore.endsWith(EOL) || gitIgnore.length === 0 ? '' : EOL
}${VERCEL_DIR}${EOL}`;
contentModified = true;
}
if (!gitIgnore.split(EOL).includes(VERCEL_OUTPUT_DIR)) {
gitIgnore += `${
gitIgnore.endsWith(EOL) || gitIgnore.length === 0 ? '' : EOL
}${VERCEL_OUTPUT_DIR}${EOL}`;
contentModified = true;
}
if (contentModified) {
await writeFile(gitIgnorePath, gitIgnore);
isGitIgnoreUpdated = true;
}
} catch (error) {

View File

@@ -0,0 +1,3 @@
export default function () {
return 'freecandy';
}

View File

@@ -19,7 +19,7 @@ const getRevertAliasConfigFile = () => {
],
});
};
module.exports = async function prepare(session) {
module.exports = async function prepare(session, binaryPath) {
const spec = {
'static-single-file': {
'first.png': getImageFile(session, { size: 30 }),
@@ -114,6 +114,23 @@ module.exports = async function prepare(session) {
2
),
},
'dev-fail-on-recursion-command': {
'package.json': '{}',
},
'build-fail-on-recursion-command': {
'package.json': '{}',
},
'build-fail-on-recursion-script': {
'package.json': JSON.stringify(
{
scripts: {
build: `${binaryPath} build`,
},
},
null,
2
),
},
'static-deployment': {
'index.txt': 'Hello World',
},
@@ -348,6 +365,10 @@ module.exports = async function prepare(session) {
'project-link-dev': {
'package.json': '{}',
},
'project-link-gitignore': {
'package.json': '{}',
'.gitignore': '.output',
},
'project-link-legacy': {
'index.html': 'Hello',
'vercel.json': '{"builds":[{"src":"*.html","use":"@vercel/static"}]}',

View File

@@ -252,10 +252,69 @@ const createUser = async () => {
const getConfigAuthPath = () => path.join(globalDir, 'auth.json');
async function setupProject(process, projectName, overrides) {
await waitForPrompt(process, chunk => /Set up [^?]+\?/.test(chunk));
process.stdin.write('yes\n');
await waitForPrompt(process, chunk => /Which scope [^?]+\?/.test(chunk));
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Link to existing project?')
);
process.stdin.write('no\n');
await waitForPrompt(process, chunk =>
chunk.includes('Whats your projects name?')
);
process.stdin.write(`${projectName}\n`);
await waitForPrompt(process, chunk =>
chunk.includes('In which directory is your code located?')
);
process.stdin.write('\n');
await waitForPrompt(process, chunk =>
chunk.includes('Want to override the settings?')
);
if (overrides) {
process.stdin.write('yes\n');
const { buildCommand, outputDirectory, devCommand } = overrides;
await waitForPrompt(process, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
process.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Build Command?`)
);
process.stdin.write(`${buildCommand ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Output Directory?`)
);
process.stdin.write(`${outputDirectory ?? ''}\n`);
await waitForPrompt(process, chunk =>
chunk.includes(`What's your Development Command?`)
);
process.stdin.write(`${devCommand ?? ''}\n`);
} else {
process.stdin.write('no\n');
}
await waitForPrompt(process, chunk => chunk.includes('Linked to'));
}
test.before(async () => {
try {
await createUser();
await prepareFixtures(contextName);
await prepareFixtures(contextName, binaryPath);
} catch (err) {
console.log('Failed `test.before`');
console.log(err);
@@ -2227,13 +2286,93 @@ test('whoami', async t => {
t.is(stdout, contextName, formatOutput({ stdout, stderr }));
});
test('fail `now dev` dev script without now.json', async t => {
test('[vercel dev] fails when dev script calls vercel dev recursively', async t => {
const deploymentPath = fixture('now-dev-fail-dev-script');
const { exitCode, stderr } = await execute(['dev', deploymentPath]);
t.is(exitCode, 1);
t.true(
stderr.includes('must not contain `now dev`'),
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel dev] fails when development commad calls vercel dev recursively', async t => {
const dir = fixture('dev-fail-on-recursion-command');
const projectName = `dev-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const dev = execa(binaryPath, ['dev', ...defaultArgs], {
cwd: dir,
reject: false,
});
await setupProject(dev, projectName, {
devCommand: `${binaryPath} dev`,
});
const { exitCode, stderr } = await dev;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build commad calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-command');
const projectName = `build-fail-on-recursion-command-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName, {
buildCommand: `${binaryPath} build`,
});
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
test('[vercel build] fails when build script calls vercel build recursively', async t => {
const dir = fixture('build-fail-on-recursion-script');
const projectName = `build-fail-on-recursion-script-${
Math.random().toString(36).split('.')[1]
}`;
const build = execa(binaryPath, ['build', ...defaultArgs], {
cwd: dir,
reject: false,
});
await waitForPrompt(build, chunk =>
chunk.includes('No Project Settings found locally')
);
build.stdin.write('yes\n');
await setupProject(build, projectName);
const { exitCode, stderr } = await build;
t.is(exitCode, 1);
t.true(
stderr.includes('must not recursively invoke itself'),
`Received instead: "${stderr}"`
);
});
@@ -2544,7 +2683,7 @@ test('deploy a Lambda with 3 seconds of maxDuration', async t => {
const url = new URL(output.stdout);
// Should time out
url.pathname = '/api/wait-for/4';
url.pathname = '/api/wait-for/5';
const response1 = await fetch(url.href);
t.is(
response1.status,
@@ -2553,7 +2692,7 @@ test('deploy a Lambda with 3 seconds of maxDuration', async t => {
);
// Should not time out
url.pathname = '/api/wait-for/2';
url.pathname = '/api/wait-for/1';
const response2 = await fetch(url.href);
t.is(
response2.status,
@@ -2683,59 +2822,10 @@ test('should show prompts to set up project during first deploy', async t => {
const now = execa(binaryPath, [dir, ...defaultArgs]);
await waitForPrompt(now, chunk => /Set up and deploy [^?]+\?/.test(chunk));
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Link to existing project?')
);
now.stdin.write('no\n');
await waitForPrompt(now, chunk =>
chunk.includes('Whats your projects name?')
);
now.stdin.write(`${projectName}\n`);
await waitForPrompt(now, chunk =>
chunk.includes('In which directory is your code located?')
);
now.stdin.write('\n');
await waitForPrompt(now, chunk =>
chunk.includes('Want to override the settings?')
);
now.stdin.write('yes\n');
await waitForPrompt(now, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
now.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Build Command?`)
);
now.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Output Directory?`)
);
now.stdin.write(`o\n`);
await waitForPrompt(now, chunk =>
chunk.includes(`What's your Development Command?`)
);
now.stdin.write(`\n`);
await waitForPrompt(now, chunk => chunk.includes('Linked to'));
await setupProject(now, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await now;
@@ -3301,55 +3391,10 @@ test('[vc link] should show prompts to set up project', async t => {
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Which scope should contain your project?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
vc.stdin.write('no\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Whats your projects name?')
);
vc.stdin.write(`${projectName}\n`);
await waitForPrompt(vc, chunk =>
chunk.includes('In which directory is your code located?')
);
vc.stdin.write('\n');
await waitForPrompt(vc, chunk =>
chunk.includes('Want to override the settings?')
);
vc.stdin.write('yes\n');
await waitForPrompt(vc, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
vc.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Build Command?`)
);
vc.stdin.write(`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Output Directory?`)
);
vc.stdin.write(`o\n`);
await waitForPrompt(vc, chunk =>
chunk.includes(`What's your Development Command?`)
);
vc.stdin.write(`\n`);
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
await setupProject(vc, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
const output = await vc;
@@ -3408,6 +3453,29 @@ test('[vc link --confirm] should not show prompts and autolink', async t => {
);
});
test('[vc link] should not duplicate paths in .gitignore', async t => {
const dir = fixture('project-link-gitignore');
// remove previously linked project if it exists
await remove(path.join(dir, '.vercel'));
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['link', '--confirm', ...defaultArgs],
{ cwd: dir, reject: false }
);
// Ensure the exit code is right
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
// Ensure the message is correct pattern
t.regex(stderr, /Linked to /m);
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
t.is(gitignore, '.output\n.vercel\n');
});
test('[vc dev] should show prompts to set up project', async t => {
const dir = fixture('project-link-dev');
const port = 58352;
@@ -3422,59 +3490,10 @@ test('[vc dev] should show prompts to set up project', async t => {
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
});
// Ensure .gitignore is created
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
@@ -3574,59 +3593,12 @@ test('[vc dev] should send the platform proxy request headers to frontend dev se
cwd: dir,
});
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
dev.stdin.write('yes\n');
await setupProject(dev, projectName, {
buildCommand: `mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html`,
outputDirectory: 'o',
devCommand: 'node server.js',
});
await waitForPrompt(dev, chunk =>
chunk.includes('Which scope should contain your project?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Link to existing project?')
);
dev.stdin.write('no\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Whats your projects name?')
);
dev.stdin.write(`${projectName}\n`);
await waitForPrompt(dev, chunk =>
chunk.includes('In which directory is your code located?')
);
dev.stdin.write('\n');
await waitForPrompt(dev, chunk =>
chunk.includes('Want to override the settings?')
);
dev.stdin.write('yes\n');
await waitForPrompt(dev, chunk =>
chunk.includes(
'Which settings would you like to overwrite (select multiple)?'
)
);
dev.stdin.write('a\n'); // 'a' means select all
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Build Command?`)
);
dev.stdin.write(
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Output Directory?`)
);
dev.stdin.write(`o\n`);
await waitForPrompt(dev, chunk =>
chunk.includes(`What's your Development Command?`)
);
dev.stdin.write(`node server.js\n`);
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
// Ensure that `vc dev` also works

View File

@@ -374,4 +374,15 @@ describe('DevServer', () => {
);
})
);
it(
'should render an error page when the middleware returns not a Response',
testFixture('edge-middleware-invalid-response', async server => {
const response = await fetch(`${server.address}/index.html`);
const body = await response.text();
expect(body).toStrictEqual(
'A server error has occurred\n\nEDGE_FUNCTION_INVOCATION_FAILED\n'
);
})
);
});

View File

@@ -5,7 +5,7 @@ describe('getUpdateCommand', () => {
it('should detect update command', async () => {
const updateCommand = await getUpdateCommand();
expect(updateCommand).toEqual(
`yarn add vercel@${isCanary() ? 'canary' : 'latest'}`
`npm i vercel@${isCanary() ? 'canary' : 'latest'}`
);
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "10.2.3-canary.15",
"version": "10.2.3-canary.21",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -40,7 +40,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.14",
"@vercel/build-utils": "2.12.3-canary.20",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.5.1-canary.10",
"version": "0.5.1-canary.12",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [
@@ -20,7 +20,7 @@
"@types/js-yaml": "3.12.1",
"@types/node": "12.0.4",
"@types/node-fetch": "2.5.8",
"@vercel/routing-utils": "1.11.4-canary.5",
"@vercel/routing-utils": "1.11.4-canary.6",
"ajv": "6.12.2",
"typescript": "4.3.4"
}

View File

@@ -43,6 +43,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `blitz build`',
value: 'blitz build',
},
devCommand: {
value: 'blitz start',
@@ -51,8 +52,6 @@ export const frameworks = [
placeholder: 'Next.js default',
},
},
devCommand: 'blitz start',
buildCommand: 'blitz build',
getFsOutputDir: async () => '.next',
getOutputDirName: async () => 'public',
},
@@ -83,6 +82,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `next build`',
value: 'next build',
},
devCommand: {
value: 'next dev --port $PORT',
@@ -98,8 +98,6 @@ export const frameworks = [
dependencies: ['next-plugin-sentry', 'next-sentry-source-maps'],
},
],
devCommand: 'next dev --port $PORT',
buildCommand: 'next build',
getFsOutputDir: async () => '.next',
getOutputDirName: async () => 'public',
cachePattern: '.next/cache/**',
@@ -131,6 +129,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `gatsby build`',
value: 'gatsby build',
},
devCommand: {
value: 'gatsby develop --port $PORT',
@@ -141,8 +140,6 @@ export const frameworks = [
},
},
dependency: 'gatsby',
devCommand: 'gatsby develop --port $PORT',
buildCommand: 'gatsby build',
getOutputDirName: async () => 'public',
getFsOutputDir: async () => 'public',
defaultRoutes: async (dirPrefix: string) => {
@@ -219,6 +216,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `hexo generate`',
value: 'hexo generate',
},
devCommand: {
value: 'hexo server --port $PORT',
@@ -229,8 +227,6 @@ export const frameworks = [
},
},
dependency: 'hexo',
devCommand: 'hexo server --port $PORT',
buildCommand: 'hexo generate',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -259,6 +255,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `npx @11ty/eleventy`',
value: 'npx @11ty/eleventy',
},
devCommand: {
value: 'npx @11ty/eleventy --serve --watch --port $PORT',
@@ -269,8 +266,6 @@ export const frameworks = [
},
},
dependency: '@11ty/eleventy',
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
buildCommand: 'npx @11ty/eleventy',
getFsOutputDir: async () => '_site',
getOutputDirName: async () => '_site',
cachePattern: '.cache/**',
@@ -300,6 +295,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `docusaurus build`',
value: 'docusaurus build',
},
devCommand: {
value: 'docusaurus start --port $PORT',
@@ -310,8 +306,6 @@ export const frameworks = [
},
},
dependency: '@docusaurus/core',
devCommand: 'docusaurus start --port $PORT',
buildCommand: 'docusaurus build',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
@@ -456,6 +450,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `docusaurus-build`',
value: 'docusaurus-build',
},
devCommand: {
value: 'docusaurus-start --port $PORT',
@@ -466,8 +461,6 @@ export const frameworks = [
},
},
dependency: 'docusaurus',
devCommand: 'docusaurus-start --port $PORT',
buildCommand: 'docusaurus-build',
getFsOutputDir: async (dirPrefix: string) => {
const base = 'build';
try {
@@ -523,6 +516,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `preact build`',
value: 'preact build',
},
devCommand: {
value: 'preact watch --port $PORT',
@@ -533,8 +527,6 @@ export const frameworks = [
},
},
dependency: 'preact-cli',
devCommand: 'preact watch --port $PORT',
buildCommand: 'preact build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -581,6 +573,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `dojo build`',
value: 'dojo build',
},
devCommand: {
value: 'dojo build -m dev -w -s -p $PORT',
@@ -591,8 +584,6 @@ export const frameworks = [
},
},
dependency: '@dojo/cli',
devCommand: 'dojo build -m dev -w -s -p $PORT',
buildCommand: 'dojo build',
getFsOutputDir: async () => 'output/dist',
getOutputDirName: async () => join('output', 'dist'),
defaultRoutes: [
@@ -649,6 +640,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ember build`',
value: 'ember build',
},
devCommand: {
value: 'ember serve --port $PORT',
@@ -659,8 +651,6 @@ export const frameworks = [
},
},
dependency: 'ember-cli',
devCommand: 'ember serve --port $PORT',
buildCommand: 'ember build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -705,6 +695,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `vue-cli-service build`',
value: 'vue-cli-service build',
},
devCommand: {
value: 'vue-cli-service serve --port $PORT',
@@ -715,8 +706,6 @@ export const frameworks = [
},
},
dependency: '@vue/cli-service',
devCommand: 'vue-cli-service serve --port $PORT',
buildCommand: 'vue-cli-service build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -783,6 +772,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build && scully`',
value: 'ng build && scully',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -793,8 +783,6 @@ export const frameworks = [
},
},
dependency: '@scullyio/init',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build && scully',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist/static',
},
@@ -822,6 +810,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build`',
value: 'ng build',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -831,8 +820,6 @@ export const frameworks = [
},
},
dependency: '@ionic/angular',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www',
defaultRoutes: [
@@ -876,6 +863,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `ng build`',
value: 'ng build',
},
devCommand: {
value: 'ng serve --port $PORT',
@@ -886,8 +874,6 @@ export const frameworks = [
},
},
dependency: '@angular/cli',
devCommand: 'ng serve --port $PORT',
buildCommand: 'ng build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async (dirPrefix: string) => {
const base = 'dist';
@@ -945,6 +931,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `polymer build`',
value: 'polymer build',
},
devCommand: {
value: 'polymer serve --port $PORT',
@@ -955,8 +942,6 @@ export const frameworks = [
},
},
dependency: 'polymer-cli',
devCommand: 'polymer serve --port $PORT',
buildCommand: 'polymer build',
getFsOutputDir: async () => 'build',
getOutputDirName: async (dirPrefix: string) => {
const base = 'build';
@@ -1016,6 +1001,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `rollup -c`',
value: 'rollup -c',
},
devCommand: {
value: 'rollup -c -w',
@@ -1025,8 +1011,6 @@ export const frameworks = [
},
},
dependency: 'sirv-cli',
devCommand: 'rollup -c -w',
buildCommand: 'rollup -c',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultRoutes: [
@@ -1070,6 +1054,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `svelte-kit build`',
value: 'svelte-kit build',
},
devCommand: {
value: 'svelte-kit dev --port $PORT',
@@ -1079,8 +1064,6 @@ export const frameworks = [
placeholder: 'public',
},
},
devCommand: 'svelte-kit dev --port $PORT',
buildCommand: 'svelte-kit build',
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'public',
},
@@ -1108,6 +1091,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `react-scripts build`',
value: 'react-scripts build',
},
devCommand: {
value: 'react-scripts start',
@@ -1117,8 +1101,6 @@ export const frameworks = [
},
},
dependency: '@ionic/react',
devCommand: 'react-scripts start',
buildCommand: 'react-scripts build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -1216,6 +1198,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `react-scripts build`',
value: 'react-scripts build',
},
devCommand: {
value: 'react-scripts start',
@@ -1225,8 +1208,6 @@ export const frameworks = [
},
},
dependency: 'react-scripts',
devCommand: 'react-scripts start',
buildCommand: 'react-scripts build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
defaultRoutes: [
@@ -1318,6 +1299,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `gridsome build`',
value: 'gridsome build',
},
devCommand: {
value: 'gridsome develop -p $PORT',
@@ -1328,8 +1310,6 @@ export const frameworks = [
},
},
dependency: 'gridsome',
devCommand: 'gridsome develop -p $PORT',
buildCommand: 'gridsome build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
},
@@ -1357,6 +1337,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `umi build`',
value: 'umi build',
},
devCommand: {
value: 'umi dev --port $PORT',
@@ -1367,8 +1348,6 @@ export const frameworks = [
},
},
dependency: 'umi',
devCommand: 'umi dev --port $PORT',
buildCommand: 'umi build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -1412,6 +1391,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `sapper export`',
value: 'sapper export',
},
devCommand: {
value: 'sapper dev --port $PORT',
@@ -1422,8 +1402,6 @@ export const frameworks = [
},
},
dependency: 'sapper',
devCommand: 'sapper dev --port $PORT',
buildCommand: 'sapper export',
getFsOutputDir: async () => '__sapper__/export',
getOutputDirName: async () => '__sapper__/export',
},
@@ -1451,6 +1429,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `saber build`',
value: 'saber build',
},
devCommand: {
value: 'saber --port $PORT',
@@ -1461,8 +1440,6 @@ export const frameworks = [
},
},
dependency: 'saber',
devCommand: 'saber --port $PORT',
buildCommand: 'saber build',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultRoutes: [
@@ -1521,6 +1498,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `stencil build`',
value: 'stencil build',
},
devCommand: {
value: 'stencil build --dev --watch --serve --port $PORT',
@@ -1531,8 +1509,6 @@ export const frameworks = [
},
},
dependency: '@stencil/core',
devCommand: 'stencil build --dev --watch --serve --port $PORT',
buildCommand: 'stencil build',
getFsOutputDir: async () => 'www',
getOutputDirName: async () => 'www',
defaultRoutes: [
@@ -1611,6 +1587,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `nuxt generate`',
value: 'nuxt generate',
},
devCommand: {
value: 'nuxt',
@@ -1620,8 +1597,6 @@ export const frameworks = [
},
},
dependency: 'nuxt',
devCommand: 'nuxt',
buildCommand: 'nuxt generate',
getFsOutputDir: async () => '.output',
getOutputDirName: async () => 'dist',
cachePattern: '.nuxt/**',
@@ -1680,8 +1655,6 @@ export const frameworks = [
placeholder: 'RedwoodJS default',
},
},
devCommand: 'yarn rw dev --fwd="--port=$PORT --open=false',
buildCommand: 'yarn rw deploy vercel',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -1717,6 +1690,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `hugo -D --gc`',
value: 'hugo -D --gc',
},
devCommand: {
value: 'hugo server -D -w -p $PORT',
@@ -1726,8 +1700,6 @@ export const frameworks = [
placeholder: '`public` or `publishDir` from the `config` file',
},
},
devCommand: 'hugo server -D -w -p $PORT',
buildCommand: 'hugo -D --gc',
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type HugoConfig = { publishDir?: string };
const config = await readConfigFile<HugoConfig>(
@@ -1772,6 +1744,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `jekyll build`',
value: 'jekyll build',
},
devCommand: {
value: 'bundle exec jekyll serve --watch --port $PORT',
@@ -1781,8 +1754,6 @@ export const frameworks = [
placeholder: '`_site` or `destination` from `_config.yml`',
},
},
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
buildCommand: 'jekyll build',
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
type JekyllConfig = { destination?: string };
const config = await readConfigFile<JekyllConfig>(
@@ -1821,6 +1792,7 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `brunch build --production`',
value: 'brunch build --production',
},
devCommand: {
value: 'brunch watch --server --port $PORT',
@@ -1830,8 +1802,6 @@ export const frameworks = [
value: 'public',
},
},
devCommand: 'brunch watch --server --port $PORT',
buildCommand: 'brunch build --production',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
},
@@ -1856,18 +1826,17 @@ export const frameworks = [
value: 'bundle install',
},
buildCommand: {
value: '`npm run build` or `bundle exec middleman build`',
placeholder: '`npm run build` or `bundle exec middleman build`',
value: 'bundle exec middleman build',
},
devCommand: {
value: 'bundle exec middleman server -p $PORT',
placeholder: 'bundle exec middleman server',
value: 'bundle exec middleman server -p $PORT',
},
outputDirectory: {
value: 'build',
},
},
devCommand: 'bundle exec middleman server -p $PORT',
buildCommand: 'bundle exec middleman build',
getFsOutputDir: async () => 'build',
getOutputDirName: async () => 'build',
cachePattern: '{vendor/bin,vendor/cache,vendor/bundle}/**',
@@ -1896,15 +1865,13 @@ export const frameworks = [
value: 'zola build',
},
devCommand: {
value: 'zola serve --port $PORT',
placeholder: 'zola serve',
value: 'zola serve --port $PORT',
},
outputDirectory: {
value: 'public',
},
},
devCommand: 'zola serve --port $PORT',
buildCommand: 'zola build',
getFsOutputDir: async () => 'public',
getOutputDirName: async () => 'public',
defaultVersion: '0.13.0',
@@ -1934,17 +1901,17 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `vite build`',
value: 'vite build',
},
devCommand: {
placeholder: 'vite',
value: 'vite',
},
outputDirectory: {
value: 'dist',
},
},
dependency: 'vite',
devCommand: 'vite',
buildCommand: 'vite build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
},
@@ -1972,17 +1939,17 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run build` or `parcel build`',
value: 'parcel build',
},
devCommand: {
placeholder: 'parcel',
value: 'parcel',
},
outputDirectory: {
placeholder: 'dist',
value: 'dist',
},
},
dependency: 'parcel',
devCommand: 'parcel',
buildCommand: 'parcel build',
getFsOutputDir: async () => 'dist',
getOutputDirName: async () => 'dist',
defaultRoutes: [
@@ -2016,16 +1983,16 @@ export const frameworks = [
},
buildCommand: {
placeholder: '`npm run vercel-build` or `npm run build`',
value: null,
},
devCommand: {
placeholder: 'None',
value: null,
},
outputDirectory: {
placeholder: '`public` if it exists, or `.`',
},
},
devCommand: null,
buildCommand: null,
getFsOutputDir: async (dirPrefix: string): Promise<string> => {
// Public if it exists or `.`
let base = 'public';

View File

@@ -26,7 +26,7 @@ export interface SettingValue {
* A predefined setting for the detected framework
* @example "next dev --port $PORT"
*/
value: string;
value: string | null;
placeholder?: string;
}
@@ -129,11 +129,11 @@ export interface Framework {
/**
* Default Build Command or a placeholder
*/
buildCommand: Setting;
buildCommand: SettingValue;
/**
* Default Development Command or a placeholder
*/
devCommand: Setting;
devCommand: SettingValue;
/**
* Default Output Directory
*/
@@ -157,6 +157,7 @@ export interface Framework {
/**
* Name of a dependency in `package.json` to detect this framework.
* @example "hexo"
* @deprecated use `detectors` instead (new frameworks should not use this prop)
*/
dependency?: string;
/**
@@ -201,16 +202,6 @@ export interface Framework {
* @example ".cache/**"
*/
cachePattern?: string;
/**
* The default build command for the framework.
* @example "next build"
*/
buildCommand: string | null;
/**
* The default development command for the framework.
* @example "next dev"
*/
devCommand: string | null;
/**
* The default version of the framework command that is available within the
* build image. Usually an environment variable can be set to override this.

View File

@@ -34,7 +34,7 @@ const SchemaSettings = {
additionalProperties: false,
properties: {
value: {
type: 'string',
type: ['string', 'null'],
},
placeholder: {
type: 'string',
@@ -58,15 +58,7 @@ const Schema = {
type: 'array',
items: {
type: 'object',
required: [
'name',
'slug',
'logo',
'description',
'settings',
'buildCommand',
'devCommand',
],
required: ['name', 'slug', 'logo', 'description', 'settings'],
properties: {
name: { type: 'string' },
slug: { type: ['string', 'null'] },
@@ -138,8 +130,6 @@ const Schema = {
dependency: { type: 'string' },
cachePattern: { type: 'string' },
buildCommand: { type: ['string', 'null'] },
devCommand: { type: ['string', 'null'] },
defaultVersion: { type: 'string' },
},
},

View File

@@ -6,7 +6,8 @@ import { join } from 'path';
import stringArgv from 'string-argv';
import { debug } from '@vercel/build-utils';
const versionMap = new Map([
['1.16', '1.16'],
['1.17', '1.17.3'],
['1.16', '1.16.10'],
['1.15', '1.15.8'],
['1.14', '1.14.15'],
['1.13', '1.13.15'],

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "1.2.4-canary.3",
"version": "1.2.4-canary.4",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",

View File

@@ -5,10 +5,10 @@
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
],
"probes": [
{ "path": "/", "mustContain": "cow:go1.16:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "mustContain": "cow:go1.17.3:RANDOMNESS_PLACEHOLDER" },
{
"path": "/subdirectory",
"mustContain": "subcow:go1.16:RANDOMNESS_PLACEHOLDER"
"mustContain": "subcow:go1.17.3:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -1,3 +1,3 @@
module with-nested
go 1.12
go 1.16

View File

@@ -13,7 +13,9 @@ async function main() {
await execa(
'ncc',
['build', join(srcDir, 'index.ts'), '-o', outDir, '--external', 'esbuild'],
{ stdio: 'inherit' }
{
stdio: 'inherit',
}
);
await fs.copyFile(

View File

@@ -1,6 +1,6 @@
{
"name": "vercel-plugin-middleware",
"version": "0.0.0-canary.5",
"version": "0.0.0-canary.7",
"license": "MIT",
"main": "./dist/index",
"homepage": "",
@@ -17,6 +17,9 @@
"files": [
"dist"
],
"dependencies": {
"esbuild": "0.13.12"
},
"devDependencies": {
"@peculiar/webcrypto": "1.2.0",
"@types/cookie": "0.4.1",
@@ -29,7 +32,6 @@
"@types/uuid": "8.3.1",
"@vercel/ncc": "0.24.0",
"cookie": "0.4.1",
"esbuild": "0.13.10",
"formdata-node": "4.3.1",
"glob": "7.2.0",
"http-proxy": "1.18.1",

View File

@@ -3,13 +3,16 @@ _ENTRIES = typeof _ENTRIES === 'undefined' ? {} : _ENTRIES;
_ENTRIES['middleware_pages/_middleware'] = {
default: async function (ev) {
const result = await middleware.default(ev.request, ev);
if (result instanceof Response) {
return {
promise: Promise.resolve(),
waitUntil: Promise.resolve(),
response: result,
response:
result ||
new Response(null, {
headers: {
'x-middleware-next': 1,
},
}),
};
}
return result;
},
};

View File

@@ -295,11 +295,6 @@ async function runMiddleware(params: {
console.error(`Uncaught: middleware waitUntil errored`, error);
});
// TODO - is this needed?
// if (!result) {
// this.send404(params.request, params.response, params.requestId);
// }
return result;
}

View File

@@ -1,8 +1,19 @@
import { join } from 'path';
import { promises as fsp } from 'fs';
import { build } from '../src';
import { Response } from 'node-fetch';
describe('build()', () => {
beforeEach(() => {
//@ts-ignore
global.Response = Response;
});
afterEach(() => {
//@ts-ignore
delete global.Response;
//@ts-ignore
delete global._ENTRIES;
});
it('should build simple middleware', async () => {
const fixture = join(__dirname, 'fixtures/simple');
await build({
@@ -17,8 +28,30 @@ describe('build()', () => {
);
expect(middlewareManifest).toMatchSnapshot();
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
expect(await fsp.stat(outputFile)).toBeTruthy();
require(outputFile);
//@ts-ignore
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
expect(typeof middleware).toStrictEqual('function');
const handledResponse = await middleware({
request: {
url: 'http://google.com',
},
});
const unhandledResponse = await middleware({
request: {
url: 'literallyanythingelse',
},
});
expect(String(handledResponse.response.body)).toEqual('Hi from the edge!');
expect(
await fsp.stat(join(fixture, '.output/server/pages/_middleware.js'))
).toBeTruthy();
(handledResponse.response as Response).headers.get('x-middleware-next')
).toEqual(null);
expect(unhandledResponse.response.body).toEqual(null);
expect(
(unhandledResponse.response as Response).headers.get('x-middleware-next')
).toEqual('1');
});
});

View File

@@ -1 +1,5 @@
export default () => new Response('Hi from the edge!');
export default req => {
if (req.url === 'http://google.com') {
return new Response('Hi from the edge!');
}
};

View File

@@ -1,5 +1,6 @@
const { parse } = require('url');
const { parse, pathToFileURL } = require('url');
const { createServer, Server } = require('http');
const { isAbsolute } = require('path');
const { Bridge } = require('./bridge.js');
/**
@@ -15,8 +16,9 @@ function makeVercelLauncher(config) {
shouldAddSourcemapSupport = false,
} = config;
return `
const { parse } = require('url');
const { parse, pathToFileURL } = require('url');
const { createServer, Server } = require('http');
const { isAbsolute } = require('path');
const { Bridge } = require(${JSON.stringify(bridgePath)});
${
shouldAddSourcemapSupport
@@ -60,13 +62,15 @@ function getVercelLauncher({
process.env.NODE_ENV = region === 'dev1' ? 'development' : 'production';
}
async function getListener() {
/**
* @param {string} p - entrypointPath
*/
async function getListener(p) {
let listener = useRequire
? require(entrypointPath)
: await import(entrypointPath);
? require(p)
: await import(isAbsolute(p) ? pathToFileURL(p).href : p);
// In some cases we might have nested default props
// due to TS => JS
// In some cases we might have nested default props due to TS => JS
for (let i = 0; i < 5; i++) {
if (listener.default) listener = listener.default;
}
@@ -74,7 +78,7 @@ function getVercelLauncher({
return listener;
}
getListener()
getListener(entrypointPath)
.then(listener => {
if (typeof listener.listen === 'function') {
Server.prototype.listen = originalListen;

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node-bridge",
"version": "2.1.1-canary.1",
"version": "2.1.1-canary.2",
"license": "MIT",
"main": "./index.js",
"repository": {

View File

@@ -33,14 +33,6 @@ async function main() {
await fs.remove(symlinkTarget);
await fs.symlink('symlinked-asset', symlinkTarget);
// Use types.d.ts as the main types export
await Promise.all(
(await fs.readdir(outDir))
.filter(p => p.endsWith('.d.ts') && p !== 'types.d.ts')
.map(p => fs.remove(join(outDir, p)))
);
await fs.rename(join(outDir, 'types.d.ts'), join(outDir, 'index.d.ts'));
// Bundle helpers.ts with ncc
await fs.remove(join(outDir, 'helpers.js'));
const helpersDir = join(outDir, 'helpers');

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "1.12.2-canary.4",
"version": "1.12.2-canary.7",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -34,7 +34,7 @@
"@types/test-listen": "1.1.0",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0",
"@vercel/node-bridge": "2.1.1-canary.1",
"@vercel/node-bridge": "2.1.1-canary.2",
"content-type": "1.0.4",
"cookie": "0.4.0",
"etag": "1.8.1",

View File

@@ -339,6 +339,9 @@ function getAWSLambdaHandler(entrypoint: string, config: Config) {
return '';
}
// Ensures that everything from `types.ts` is exported in the final `index.d.ts` file.
export * from './types';
export const version = 3;
export async function build({

View File

@@ -16,7 +16,7 @@ const init = async () => {
console.log('Hapi server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
process.on('unhandledRejection', err => {
console.log('Hapi failed in an unexpected way');
console.log(err);
process.exit(1);

View File

@@ -3,10 +3,10 @@ const path = require('path');
module.exports = (req, resp) => {
const asset1 = fs.readFileSync(
path.join(__dirname, 'subdirectory1/asset.txt'),
path.join(__dirname, 'subdirectory1/asset.txt')
);
const asset2 = fs.readFileSync(
path.join(__dirname, 'subdirectory2/asset.txt'),
path.join(__dirname, 'subdirectory2/asset.txt')
);
resp.end(`${asset1},${asset2}`);
};

View File

@@ -8,8 +8,8 @@ const typeDefs = `
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name || "world"}`
}
hello: (_, { name }) => `Hello ${name || 'world'}`,
},
};
const lambda = new GraphQLServerLambda({

View File

@@ -0,0 +1,27 @@
{
"private": false,
"name": "vercel-plugin-go",
"version": "1.0.0-canary.5",
"main": "dist/index.js",
"license": "MIT",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/vercel/vercel.git",
"directory": "packages/vercel-plugin-go"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "tsc"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.20",
"@vercel/go": "1.2.4-canary.4"
},
"devDependencies": {
"@types/node": "*",
"typescript": "4.3.4"
}
}

View File

@@ -0,0 +1,6 @@
import { convertRuntimeToPlugin } from '@vercel/build-utils';
import * as go from '@vercel/go';
export const build = convertRuntimeToPlugin(go.build, '.go');
export const startDevServer = go.startDevServer;

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"declaration": false,
"esModuleInterop": true,
"lib": ["esnext"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"strict": true,
"target": "esnext"
}
}

6
packages/plugin-node/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
/test/fixtures/**/.env
/test/fixtures/**/.gitignore
/test/fixtures/**/.output
/test/fixtures/**/types.d.ts
/test/fixtures/11-symlinks/symlink

View File

@@ -0,0 +1,45 @@
declare function ncc(
entrypoint: string,
options?: ncc.NccOptions
): ncc.NccResult;
declare namespace ncc {
export interface NccOptions {
watch?: any;
sourceMap?: boolean;
sourceMapRegister?: boolean;
}
export interface Asset {
source: Buffer;
permissions: number;
}
export interface Assets {
[name: string]: Asset;
}
export interface BuildResult {
err: Error | null | undefined;
code: string;
map: string | undefined;
assets: Assets | undefined;
permissions: number | undefined;
}
export type HandlerFn = (params: BuildResult) => void;
export type HandlerCallback = (fn: HandlerFn) => void;
export type RebuildFn = () => void;
export type RebuildCallback = (fn: RebuildFn) => void;
export type CloseCallback = () => void;
export interface NccResult {
handler: HandlerCallback;
rebuild: RebuildCallback;
close: CloseCallback;
}
}
declare module '@vercel/ncc' {
export = ncc;
}

1
packages/plugin-node/bench/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
lambda

View File

@@ -0,0 +1,19 @@
const express = require('express');
const app = express();
module.exports = app;
app.use(express.json());
app.post('*', (req, res) => {
if (req.body == null) {
return res.status(400).send({ error: 'no JSON object in the request' });
}
return res.status(200).send(JSON.stringify(req.body, null, 4));
});
app.all('*', (req, res) => {
res.status(405).send({ error: 'only POST requests are accepted' });
});

View File

@@ -0,0 +1,7 @@
module.exports = (req, res) => {
if (req.body == null) {
return res.status(400).send({ error: 'no JSON object in the request' });
}
return res.status(200).send(JSON.stringify(req.body, null, 4));
};

View File

@@ -0,0 +1,9 @@
function doNothing() {}
module.exports = (req, res) => {
doNothing(req.query.who);
doNothing(req.body);
doNothing(req.cookies);
res.end('hello');
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello');
};

View File

@@ -0,0 +1,10 @@
{
"name": "bench",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"express": "4.17.1",
"fs-extra": "8.0.1"
}
}

View File

@@ -0,0 +1,91 @@
const fs = require('fs-extra');
const { join } = require('path');
const { makeLauncher } = require('../dist/launcher');
const setupFiles = async (entrypoint, shouldAddHelpers) => {
await fs.remove(join(__dirname, 'lambda'));
await fs.ensureDir(join(__dirname, 'lambda'));
await fs.copyFile(
join(__dirname, '../dist/helpers.js'),
join(__dirname, 'lambda/helpers.js')
);
await fs.copyFile(
require.resolve('@vercel/node-bridge/bridge'),
join(__dirname, 'lambda/bridge.js')
);
await fs.copyFile(
join(process.cwd(), entrypoint),
join(__dirname, 'lambda/entrypoint.js')
);
let launcher = makeLauncher('./entrypoint', shouldAddHelpers);
launcher += '\nexports.bridge=bridge';
await fs.writeFile(join(__dirname, 'lambda/launcher.js'), launcher);
};
const createBigJSONObj = () => {
const obj = {};
for (let i = 0; i < 1000; i += 1) {
obj[`idx${i}`] = `val${i}`;
}
};
const createEvent = () => ({
Action: 'Invoke',
body: JSON.stringify({
method: 'POST',
path: '/',
headers: { 'content-type': 'application/json' },
encoding: undefined,
body: createBigJSONObj(),
}),
});
const runTests = async (entrypoint, shouldAddHelpers = true, nb) => {
console.log(
`setting up files with entrypoint ${entrypoint} and ${
shouldAddHelpers ? 'helpers' : 'no helpers'
}`
);
await setupFiles(entrypoint, shouldAddHelpers);
console.log('importing launcher');
const launcher = require('./lambda/launcher');
const event = createEvent();
const context = {};
const start = process.hrtime();
console.log(`throwing ${nb} events at lambda`);
for (let i = 0; i < nb; i += 1) {
// eslint-disable-next-line
await launcher.launcher(event, context);
}
const timer = process.hrtime(start);
const ms = (timer[0] * 1e9 + timer[1]) / 1e6;
await launcher.bridge.server.close();
delete require.cache[require.resolve('./lambda/launcher')];
console.log({ nb, sum: ms, avg: ms / nb });
};
const main = async () => {
if (process.argv.length !== 5) {
console.log(
'usage : node run.js <entrypoint-file> <add-helpers> <nb-of-request>'
);
return;
}
const [, , entrypoint, helpers, nbRequests] = process.argv;
const shouldAddHelpers = helpers !== 'false' && helpers !== 'no';
const nb = Number(nbRequests);
await runTests(entrypoint, shouldAddHelpers, nb);
};
main();

View File

@@ -0,0 +1,378 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
fs-extra@8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b"
integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
http-errors@1.7.2, http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ipaddr.js@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.40.0:
version "1.40.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
mime-types@~2.1.24:
version "2.1.24"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
dependencies:
mime-db "1.40.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.0"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env node
const fs = require('fs-extra');
const execa = require('execa');
const { join } = require('path');
async function main() {
const srcDir = join(__dirname, 'src');
const outDir = join(__dirname, 'dist');
const bridgeDir = join(__dirname, '../node-bridge');
// Start fresh
await fs.remove(outDir);
// Build TypeScript files
await execa('tsc', [], {
stdio: 'inherit',
});
// Copy bridge and launcher as-is
await Promise.all([
fs.copyFile(join(bridgeDir, 'bridge.js'), join(outDir, 'bridge.js')),
fs.copyFile(join(bridgeDir, 'launcher.js'), join(outDir, 'launcher.js')),
]);
// Setup symlink for symlink test
const symlinkTarget = join(__dirname, 'test/fixtures/11-symlinks/symlink');
await fs.remove(symlinkTarget);
await fs.symlink('symlinked-asset', symlinkTarget);
// Use types.d.ts as the main types export
await Promise.all(
(await fs.readdir(outDir))
.filter(p => p.endsWith('.d.ts') && p !== 'types.d.ts')
.map(p => fs.remove(join(outDir, p)))
);
await fs.rename(join(outDir, 'types.d.ts'), join(outDir, 'index.d.ts'));
// Bundle helpers.ts with ncc
await fs.remove(join(outDir, 'helpers.js'));
const helpersDir = join(outDir, 'helpers');
await execa(
'ncc',
[
'build',
join(srcDir, 'helpers.ts'),
'-e',
'@vercel/node-bridge',
'-e',
'@vercel/build-utils',
'-e',
'typescript',
'-o',
helpersDir,
],
{ stdio: 'inherit' }
);
await fs.rename(join(helpersDir, 'index.js'), join(outDir, 'helpers.js'));
await fs.remove(helpersDir);
// Build source-map-support/register for source maps
const sourceMapSupportDir = join(outDir, 'source-map-support');
await execa(
'ncc',
[
'build',
join(__dirname, '../../node_modules/source-map-support/register'),
'-e',
'@vercel/node-bridge',
'-e',
'@vercel/build-utils',
'-e',
'typescript',
'-o',
sourceMapSupportDir,
],
{ stdio: 'inherit' }
);
await fs.rename(
join(sourceMapSupportDir, 'index.js'),
join(outDir, 'source-map-support.js')
);
await fs.remove(sourceMapSupportDir);
const mainDir = join(outDir, 'main');
await execa(
'ncc',
[
'build',
join(srcDir, 'index.ts'),
'-e',
'@vercel/node-bridge',
'-e',
'@vercel/build-utils',
'-e',
'typescript',
'-o',
mainDir,
],
{ stdio: 'inherit' }
);
await fs.rename(join(mainDir, 'index.js'), join(outDir, 'index.js'));
await fs.remove(mainDir);
await fs.remove(join(outDir, 'example-import.js'));
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,68 @@
{
"name": "vercel-plugin-node",
"version": "1.12.2-canary.11",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
"repository": {
"type": "git",
"url": "https://github.com/vercel/vercel.git",
"directory": "packages/node"
},
"scripts": {
"build": "node build",
"test-unit": "jest --env node --verbose --runInBand --bail",
"prepublishOnly": "node build"
},
"files": [
"dist"
],
"dependencies": {
"@types/node": "*",
"ts-node": "8.9.1",
"typescript": "4.3.4"
},
"devDependencies": {
"@babel/core": "7.5.0",
"@babel/plugin-transform-modules-commonjs": "7.5.0",
"@tootallnate/once": "2.0.0",
"@types/aws-lambda": "8.10.19",
"@types/content-type": "1.1.3",
"@types/cookie": "0.3.3",
"@types/etag": "1.8.0",
"@types/jest": "27.0.2",
"@types/node-fetch": "2",
"@types/test-listen": "1.1.0",
"@types/yazl": "2.4.2",
"@vercel/build-utils": "2.12.3-canary.20",
"@vercel/fun": "1.0.3",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0",
"@vercel/node-bridge": "2.1.1-canary.2",
"@vercel/static-config": "0.0.1-canary.0",
"abort-controller": "3.0.0",
"content-type": "1.0.4",
"cookie": "0.4.0",
"etag": "1.8.1",
"json-schema-to-ts": "1.6.4",
"node-fetch": "2",
"source-map-support": "0.5.12",
"test-listen": "1.1.0",
"ts-morph": "12.0.0",
"yazl": "2.5.1"
},
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"diagnostics": false,
"isolatedModules": true
}
},
"verbose": false,
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test/**/*.test.[jt]s"
]
}
}

View File

@@ -0,0 +1,32 @@
const babel = require('@babel/core'); // eslint-disable-line @typescript-eslint/no-var-requires
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pluginTransformModulesCommonJs = require('@babel/plugin-transform-modules-commonjs');
export function compile(
filename: string,
source: string
): { code: string; map: any } {
return babel.transform(source, {
filename,
configFile: false,
babelrc: false,
highlightCode: false,
compact: false,
sourceType: 'module',
sourceMaps: true,
parserOpts: {
plugins: [
'asyncGenerators',
'classProperties',
'classPrivateProperties',
'classPrivateMethods',
'optionalCatchBinding',
'objectRestSpread',
'numericSeparator',
'dynamicImport',
'importMeta',
],
},
plugins: [pluginTransformModulesCommonJs],
});
}

View File

@@ -0,0 +1,184 @@
const entrypoint = process.env.VERCEL_DEV_ENTRYPOINT;
delete process.env.VERCEL_DEV_ENTRYPOINT;
const tsconfig = process.env.VERCEL_DEV_TSCONFIG;
delete process.env.VERCEL_DEV_TSCONFIG;
if (!entrypoint) {
throw new Error('`VERCEL_DEV_ENTRYPOINT` must be defined');
}
import { join } from 'path';
import { register } from 'ts-node';
type TypescriptModule = typeof import('typescript');
let useRequire = false;
if (!process.env.VERCEL_DEV_IS_ESM) {
const resolveTypescript = (p: string): string => {
try {
return require.resolve('typescript', {
paths: [p],
});
} catch (_) {
return '';
}
};
const requireTypescript = (p: string): TypescriptModule => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(p) as TypescriptModule;
};
let ts: TypescriptModule | null = null;
// Assume Node.js 12 as the lowest common denominator
let target = 'ES2019';
const nodeMajor = Number(process.versions.node.split('.')[0]);
if (nodeMajor >= 14) {
target = 'ES2020';
}
// Use the project's version of Typescript if available and supports `target`
let compiler = resolveTypescript(process.cwd());
if (compiler) {
ts = requireTypescript(compiler);
if (!(target in ts.ScriptTarget)) {
ts = null;
}
}
// Otherwise fall back to using the copy that `@vercel/node` uses
if (!ts) {
compiler = resolveTypescript(join(__dirname, '..'));
ts = requireTypescript(compiler);
}
if (tsconfig) {
try {
const { config } = ts.readConfigFile(tsconfig, ts.sys.readFile);
if (config?.compilerOptions?.target) {
target = config.compilerOptions.target;
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`Error while parsing "${tsconfig}"`);
throw err;
}
}
}
register({
compiler,
compilerOptions: {
allowJs: true,
esModuleInterop: true,
jsx: 'react',
module: 'commonjs',
target,
},
project: tsconfig || undefined, // Resolve `tsconfig.json` from entrypoint dir
transpileOnly: true,
});
useRequire = true;
}
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
import { Readable } from 'stream';
import type { Bridge } from '@vercel/node-bridge/bridge';
// @ts-ignore - copied to the `dist` output as-is
import { getVercelLauncher } from './launcher.js.js';
function listen(server: Server, port: number, host: string): Promise<void> {
return new Promise(resolve => {
server.listen(port, host, () => {
resolve();
});
});
}
let bridge: Bridge | undefined = undefined;
async function main() {
const config = JSON.parse(process.env.VERCEL_DEV_CONFIG || '{}');
delete process.env.VERCEL_DEV_CONFIG;
const buildEnv = JSON.parse(process.env.VERCEL_DEV_BUILD_ENV || '{}');
delete process.env.VERCEL_DEV_BUILD_ENV;
const shouldAddHelpers = !(
config.helpers === false || buildEnv.NODEJS_HELPERS === '0'
);
const proxyServer = createServer(onDevRequest);
await listen(proxyServer, 0, '127.0.0.1');
const launcher = getVercelLauncher({
entrypointPath: join(process.cwd(), entrypoint!),
helpersPath: './helpers.js',
shouldAddHelpers,
useRequire,
});
bridge = launcher();
const address = proxyServer.address();
if (typeof process.send === 'function') {
process.send(address);
} else {
console.log('Dev server listening:', address);
}
}
export function rawBody(readable: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
let bytes = 0;
const chunks: Buffer[] = [];
readable.on('error', reject);
readable.on('data', chunk => {
chunks.push(chunk);
bytes += chunk.length;
});
readable.on('end', () => {
resolve(Buffer.concat(chunks, bytes));
});
});
}
export async function onDevRequest(
req: IncomingMessage,
res: ServerResponse
): Promise<void> {
const body = await rawBody(req);
const event = {
Action: 'Invoke',
body: JSON.stringify({
method: req.method,
path: req.url,
headers: req.headers,
encoding: 'base64',
body: body.toString('base64'),
}),
};
if (!bridge) {
res.statusCode = 500;
res.end('Bridge is not ready, please try again');
return;
}
const result = await bridge.launcher(event, {
callbackWaitsForEmptyEventLoop: false,
});
res.statusCode = result.statusCode;
for (const [key, value] of Object.entries(result.headers)) {
if (typeof value !== 'undefined') {
res.setHeader(key, value);
}
}
res.end(Buffer.from(result.body, result.encoding));
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,14 @@
// We intentionally import these types here
// which will fail at compile time if exports
// are not found in the index file
import {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
NowRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
NowResponse,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
VercelRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
VercelResponse,
} from './index';

View File

@@ -0,0 +1,309 @@
import {
VercelRequest,
VercelResponse,
VercelRequestCookies,
VercelRequestQuery,
VercelRequestBody,
} from './types';
import { Server } from 'http';
import type { Bridge } from '@vercel/node-bridge/bridge';
function getBodyParser(req: VercelRequest, body: Buffer) {
return function parseBody(): VercelRequestBody {
if (!req.headers['content-type']) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseContentType } = require('content-type');
const { type } = parseContentType(req.headers['content-type']);
if (type === 'application/json') {
try {
const str = body.toString();
return str ? JSON.parse(str) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON');
}
}
if (type === 'application/octet-stream') {
return body;
}
if (type === 'application/x-www-form-urlencoded') {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseQS } = require('querystring');
// note: querystring.parse does not produce an iterable object
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
return parseQS(body.toString());
}
if (type === 'text/plain') {
return body.toString();
}
return undefined;
};
}
function getQueryParser({ url = '/' }: VercelRequest) {
return function parseQuery(): VercelRequestQuery {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseURL } = require('url');
return parseURL(url, true).query;
};
}
function getCookieParser(req: VercelRequest) {
return function parseCookie(): VercelRequestCookies {
const header: undefined | string | string[] = req.headers.cookie;
if (!header) {
return {};
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse } = require('cookie');
return parse(Array.isArray(header) ? header.join(';') : header);
};
}
function status(res: VercelResponse, statusCode: number): VercelResponse {
res.statusCode = statusCode;
return res;
}
function redirect(
res: VercelResponse,
statusOrUrl: string | number,
url?: string
): VercelResponse {
if (typeof statusOrUrl === 'string') {
url = statusOrUrl;
statusOrUrl = 307;
}
if (typeof statusOrUrl !== 'number' || typeof url !== 'string') {
throw new Error(
`Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').`
);
}
res.writeHead(statusOrUrl, { Location: url }).end();
return res;
}
function setCharset(type: string, charset: string) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse, format } = require('content-type');
const parsed = parse(type);
parsed.parameters.charset = charset;
return format(parsed);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createETag(body: any, encoding: 'utf8' | undefined) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const etag = require('etag');
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
return etag(buf, { weak: true });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function send(
req: VercelRequest,
res: VercelResponse,
body: any
): VercelResponse {
let chunk: unknown = body;
let encoding: 'utf8' | undefined;
switch (typeof chunk) {
// string defaulting to html
case 'string':
if (!res.getHeader('content-type')) {
res.setHeader('content-type', 'text/html');
}
break;
case 'boolean':
case 'number':
case 'object':
if (chunk === null) {
chunk = '';
} else if (Buffer.isBuffer(chunk)) {
if (!res.getHeader('content-type')) {
res.setHeader('content-type', 'application/octet-stream');
}
} else {
return json(req, res, chunk);
}
break;
}
// write strings in utf-8
if (typeof chunk === 'string') {
encoding = 'utf8';
// reflect this in content-type
const type = res.getHeader('content-type');
if (typeof type === 'string') {
res.setHeader('content-type', setCharset(type, 'utf-8'));
}
}
// populate Content-Length
let len: number | undefined;
if (chunk !== undefined) {
if (Buffer.isBuffer(chunk)) {
// get length of Buffer
len = chunk.length;
} else if (typeof chunk === 'string') {
if (chunk.length < 1000) {
// just calculate length small chunk
len = Buffer.byteLength(chunk, encoding);
} else {
// convert chunk to Buffer and calculate
const buf = Buffer.from(chunk, encoding);
len = buf.length;
chunk = buf;
encoding = undefined;
}
} else {
throw new Error(
'`body` is not a valid string, object, boolean, number, Stream, or Buffer'
);
}
if (len !== undefined) {
res.setHeader('content-length', len);
}
}
// populate ETag
let etag: string | undefined;
if (
!res.getHeader('etag') &&
len !== undefined &&
(etag = createETag(chunk, encoding))
) {
res.setHeader('etag', etag);
}
// strip irrelevant headers
if (204 === res.statusCode || 304 === res.statusCode) {
res.removeHeader('Content-Type');
res.removeHeader('Content-Length');
res.removeHeader('Transfer-Encoding');
chunk = '';
}
if (req.method === 'HEAD') {
// skip body for HEAD
res.end();
} else if (encoding) {
// respond with encoding
res.end(chunk, encoding);
} else {
// respond without encoding
res.end(chunk);
}
return res;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function json(
req: VercelRequest,
res: VercelResponse,
jsonBody: any
): VercelResponse {
const body = JSON.stringify(jsonBody);
// content-type
if (!res.getHeader('content-type')) {
res.setHeader('content-type', 'application/json; charset=utf-8');
}
return send(req, res, body);
}
export class ApiError extends Error {
readonly statusCode: number;
constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
}
}
export function sendError(
res: VercelResponse,
statusCode: number,
message: string
) {
res.statusCode = statusCode;
res.statusMessage = message;
res.end();
}
function setLazyProp<T>(req: VercelRequest, prop: string, getter: () => T) {
const opts = { configurable: true, enumerable: true };
const optsReset = { ...opts, writable: true };
Object.defineProperty(req, prop, {
...opts,
get: () => {
const value = getter();
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value });
return value;
},
set: value => {
Object.defineProperty(req, prop, { ...optsReset, value });
},
});
}
export function createServerWithHelpers(
handler: (req: VercelRequest, res: VercelResponse) => void | Promise<void>,
bridge: Bridge
) {
const server = new Server(async (_req, _res) => {
const req = _req as VercelRequest;
const res = _res as VercelResponse;
try {
const reqId = req.headers['x-now-bridge-request-id'];
// don't expose this header to the client
delete req.headers['x-now-bridge-request-id'];
if (typeof reqId !== 'string') {
throw new ApiError(500, 'Internal Server Error');
}
const event = bridge.consumeEvent(reqId);
setLazyProp<VercelRequestCookies>(req, 'cookies', getCookieParser(req));
setLazyProp<VercelRequestQuery>(req, 'query', getQueryParser(req));
setLazyProp<VercelRequestBody>(
req,
'body',
getBodyParser(req, event.body)
);
res.status = statusCode => status(res, statusCode);
res.redirect = (statusOrUrl, url) => redirect(res, statusOrUrl, url);
res.send = body => send(req, res, body);
res.json = jsonBody => json(req, res, jsonBody);
await handler(req, res);
} catch (err) {
if (err instanceof ApiError) {
sendError(res, err.statusCode, err.message);
} else {
throw err;
}
}
});
return server;
}

View File

@@ -0,0 +1,689 @@
import { fork, spawn } from 'child_process';
import {
createWriteStream,
readFileSync,
lstatSync,
readlinkSync,
statSync,
promises as fsp,
} from 'fs';
import {
basename,
dirname,
extname,
join,
relative,
resolve,
sep,
parse as parsePath,
} from 'path';
import { Project } from 'ts-morph';
import once from '@tootallnate/once';
import { nodeFileTrace } from '@vercel/nft';
import {
File,
Files,
PrepareCacheOptions,
StartDevServerOptions,
StartDevServerResult,
glob,
FileBlob,
FileFsRef,
getNodeVersion,
getSpawnOptions,
shouldServe,
debug,
isSymbolicLink,
runNpmInstall,
updateFunctionsManifest,
updateRoutesManifest,
walkParentDirs,
normalizePath,
runPackageJsonScript,
} from '@vercel/build-utils';
import { FromSchema } from 'json-schema-to-ts';
import { getConfig, BaseFunctionConfigSchema } from '@vercel/static-config';
import { AbortController } from 'abort-controller';
import { Register, register } from './typescript';
import { pageToRoute } from './router/page-to-route';
import { isDynamicRoute } from './router/is-dynamic';
import crypto from 'crypto';
export { shouldServe };
export {
NowRequest,
NowResponse,
VercelRequest,
VercelResponse,
} from './types';
const require_ = eval('require');
// Load the helper files from the "dist" dir explicitly.
const DIST_DIR = join(__dirname, '..', 'dist');
const { makeVercelLauncher, makeAwsLauncher } = require_(
join(DIST_DIR, 'launcher.js')
);
interface DownloadOptions {
entrypoint: string;
workPath: string;
installedPaths?: Set<string>;
}
interface PortInfo {
port: number;
}
function isPortInfo(v: any): v is PortInfo {
return v && typeof v.port === 'number';
}
const FunctionConfigSchema = {
type: 'object',
additionalProperties: false,
properties: {
...BaseFunctionConfigSchema.properties,
helpers: {
type: 'boolean',
},
nodeVersion: {
type: 'string',
},
awsHandlerName: {
type: 'string',
},
excludeFiles: {
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
},
includeFiles: {
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
},
},
} as const;
type FunctionConfig = FromSchema<typeof FunctionConfigSchema>;
const tscPath = resolve(dirname(require_.resolve('typescript')), '../bin/tsc');
// eslint-disable-next-line no-useless-escape
const libPathRegEx = /^node_modules|[\/\\]node_modules[\/\\]/;
const LAUNCHER_FILENAME = '__launcher.js';
const BRIDGE_FILENAME = '__bridge.js';
const HELPERS_FILENAME = '__helpers.js';
const SOURCEMAP_SUPPORT_FILENAME = '__sourcemap_support.js';
async function maybeInstallAndBuild({
entrypoint,
workPath,
installedPaths,
}: DownloadOptions) {
const entrypointFsDirname = join(workPath, dirname(entrypoint));
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const spawnOpts = getSpawnOptions({}, nodeVersion);
const lastPath = await walkParentDirs({
base: workPath,
start: entrypointFsDirname,
filename: 'package.json',
});
if (!lastPath || dirname(lastPath) === workPath) {
debug(`Skip install command in \`vercel-plugin-node\` for ${entrypoint}.`);
} else if (lastPath) {
if (!installedPaths?.has(lastPath)) {
installedPaths?.add(lastPath);
const installTime = Date.now();
await runNpmInstall(dirname(lastPath), [], spawnOpts, {}, nodeVersion);
debug(
`Install complete [${Date.now() - installTime}ms] for ${relative(
workPath,
lastPath
)}`
);
await runPackageJsonScript(
dirname(lastPath),
// Don't consider "build" script since its intended for frontend code
['vercel-build', 'now-build'],
spawnOpts
);
} else {
debug(
`Skip install command in \`vercel-plugin-node\` for ${entrypoint}. Already installed for other entrypoint.`
);
}
}
return {
nodeVersion,
spawnOpts,
};
}
function renameTStoJS(path: string) {
if (path.endsWith('.ts')) {
return path.slice(0, -3) + '.js';
}
if (path.endsWith('.tsx')) {
return path.slice(0, -4) + '.js';
}
return path;
}
async function compile(
baseDir: string,
entrypointPath: string,
config: FunctionConfig
): Promise<{
preparedFiles: Files;
shouldAddSourcemapSupport: boolean;
}> {
const inputFiles = new Set<string>([entrypointPath]);
const preparedFiles: Files = {};
const sourceCache = new Map<string, string | Buffer | null>();
const fsCache = new Map<string, File>();
const tsCompiled = new Set<string>();
const pkgCache = new Map<string, { type?: string }>();
let shouldAddSourcemapSupport = false;
if (config.includeFiles) {
const includeFiles =
typeof config.includeFiles === 'string'
? [config.includeFiles]
: config.includeFiles;
const rel = includeFiles.map(f => {
return relative(baseDir, join(dirname(entrypointPath), f));
});
for (const pattern of rel) {
const files = await glob(pattern, baseDir);
await Promise.all(
Object.values(files).map(async entry => {
const { fsPath } = entry;
const relPath = relative(baseDir, fsPath);
fsCache.set(relPath, entry);
preparedFiles[relPath] = entry;
})
);
}
}
debug(
'Tracing input files: ' +
[...inputFiles].map(p => relative(baseDir, p)).join(', ')
);
let tsCompile: Register;
function compileTypeScript(path: string, source: string): string {
const relPath = relative(baseDir, path);
if (!tsCompile) {
tsCompile = register({
basePath: baseDir, // The base is the same as root now.json dir
project: path, // Resolve tsconfig.json from entrypoint dir
files: true, // Include all files such as global `.d.ts`
});
}
const { code, map } = tsCompile(source, path);
tsCompiled.add(relPath);
preparedFiles[renameTStoJS(relPath) + '.map'] = new FileBlob({
data: JSON.stringify(map),
});
source = code;
shouldAddSourcemapSupport = true;
return source;
}
const { fileList, esmFileList, warnings } = await nodeFileTrace(
[...inputFiles],
{
base: baseDir,
processCwd: baseDir,
ts: true,
mixedModules: true,
//ignore: config.excludeFiles,
readFile(fsPath: string): Buffer | string | null {
const relPath = relative(baseDir, fsPath);
const cached = sourceCache.get(relPath);
if (cached) return cached.toString();
// null represents a not found
if (cached === null) return null;
try {
let source: string | Buffer = readFileSync(fsPath);
if (fsPath.endsWith('.ts') || fsPath.endsWith('.tsx')) {
source = compileTypeScript(fsPath, source.toString());
}
const { mode } = lstatSync(fsPath);
let entry: File;
if (isSymbolicLink(mode)) {
entry = new FileFsRef({ fsPath, mode });
} else {
entry = new FileBlob({ data: source, mode });
}
fsCache.set(relPath, entry);
sourceCache.set(relPath, source);
return source.toString();
} catch (e) {
if (e.code === 'ENOENT' || e.code === 'EISDIR') {
sourceCache.set(relPath, null);
return null;
}
throw e;
}
},
}
);
for (const warning of warnings) {
if (warning && warning.stack) {
debug(warning.stack.replace('Error: ', 'Warning: '));
}
}
for (const path of fileList) {
let entry = fsCache.get(path);
if (!entry) {
const fsPath = resolve(baseDir, path);
const { mode } = lstatSync(fsPath);
if (isSymbolicLink(mode)) {
entry = new FileFsRef({ fsPath, mode });
} else {
const source = readFileSync(fsPath);
entry = new FileBlob({ data: source, mode });
}
}
if (isSymbolicLink(entry.mode) && entry.fsPath) {
// ensure the symlink target is added to the file list
const symlinkTarget = relative(
baseDir,
resolve(dirname(entry.fsPath), readlinkSync(entry.fsPath))
);
if (
!symlinkTarget.startsWith('..' + sep) &&
fileList.indexOf(symlinkTarget) === -1
) {
const stats = statSync(resolve(baseDir, symlinkTarget));
if (stats.isFile()) {
fileList.push(symlinkTarget);
}
}
}
if (tsCompiled.has(path)) {
preparedFiles[renameTStoJS(path)] = entry;
} else {
preparedFiles[path] = entry;
}
}
// Compile ES Modules into CommonJS
const esmPaths = esmFileList.filter(
file =>
!file.endsWith('.ts') &&
!file.endsWith('.tsx') &&
!file.endsWith('.mjs') &&
!file.match(libPathRegEx)
);
if (esmPaths.length) {
const babelCompile = require('./babel').compile;
for (const path of esmPaths) {
const pathDir = join(baseDir, dirname(path));
if (!pkgCache.has(pathDir)) {
const pathToPkg = await walkParentDirs({
base: baseDir,
start: pathDir,
filename: 'package.json',
});
const pkg = pathToPkg ? require_(pathToPkg) : {};
pkgCache.set(pathDir, pkg);
}
const pkg = pkgCache.get(pathDir) || {};
if (pkg.type === 'module' && path.endsWith('.js')) {
// Found parent package.json indicating this file is already ESM
// so we should not transpile to CJS.
// https://nodejs.org/api/packages.html#packages_type
continue;
}
const filename = basename(path);
const { data: source } = await FileBlob.fromStream({
stream: preparedFiles[path].toStream(),
});
const { code, map } = babelCompile(filename, source);
shouldAddSourcemapSupport = true;
preparedFiles[path] = new FileBlob({
data: `${code}\n//# sourceMappingURL=${filename}.map`,
});
delete map.sourcesContent;
preparedFiles[path + '.map'] = new FileBlob({
data: JSON.stringify(map),
});
}
}
return {
preparedFiles,
shouldAddSourcemapSupport,
};
}
function getAWSLambdaHandler(entrypoint: string, config: FunctionConfig) {
const handler = config.awsHandlerName || process.env.NODEJS_AWS_HANDLER_NAME;
if (handler) {
const { dir, name } = parsePath(entrypoint);
return `${join(dir, name)}.${handler}`;
}
}
// TODO NATE: turn this into a `@vercel/plugin-utils` helper function?
export async function build({ workPath }: { workPath: string }) {
const project = new Project();
const entrypoints = await glob('api/**/*.[jt]s', workPath);
const installedPaths = new Set<string>();
for (const entrypoint of Object.keys(entrypoints)) {
// Dotfiles are not compiled
if (entrypoint.includes('/.')) continue;
// Files starting with an `_` (or within a directory) are not compiled
if (entrypoint.includes('/_')) continue;
// Files within a `node_modules` directory are not compiled
if (entrypoint.includes('/node_modules/')) continue;
// TypeScript definition files are not compiled
if (entrypoint.endsWith('.d.ts')) continue;
const absEntrypoint = join(workPath, entrypoint);
const config =
getConfig(project, absEntrypoint, FunctionConfigSchema) || {};
// No config exported means "node", but if there is a config
// and "runtime" is defined, but it is not "node" then don't
// compile this file.
if (config.runtime && config.runtime !== 'node') {
continue;
}
await buildEntrypoint({ workPath, entrypoint, config, installedPaths });
}
}
export async function buildEntrypoint({
workPath,
entrypoint,
config,
installedPaths,
}: {
workPath: string;
entrypoint: string;
config: FunctionConfig;
installedPaths?: Set<string>;
}) {
// Unique hash that will be used as directory name for `.output`.
const entrypointHash = crypto
.createHash('sha256')
.update(entrypoint)
.digest('hex');
const outputDirPath = join(workPath, '.output');
const { dir, name } = parsePath(entrypoint);
const entrypointWithoutExt = join('/', dir, name);
const outputWorkPath = join(outputDirPath, 'inputs', entrypointHash);
const pagesDir = join(outputDirPath, 'server', 'pages');
const pageOutput = join(pagesDir, renameTStoJS(entrypoint));
const nftOutput = `${pageOutput}.nft.json`;
await fsp.mkdir(outputWorkPath, { recursive: true });
await fsp.mkdir(parsePath(pageOutput).dir, { recursive: true });
console.log(`Compiling "${entrypoint}" to "${outputWorkPath}"`);
const shouldAddHelpers =
config.helpers !== false && process.env.NODEJS_HELPERS !== '0';
const awsLambdaHandler = getAWSLambdaHandler(entrypoint, config);
const { nodeVersion } = await maybeInstallAndBuild({
entrypoint,
workPath,
installedPaths,
});
const entrypointPath = join(workPath, entrypoint);
debug('Tracing input files...');
const traceTime = Date.now();
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
workPath,
entrypointPath,
config
);
debug(`Trace complete [${Date.now() - traceTime}ms]`);
// Has to be in `dirname(entrypoint)` because the `handler` will be prefixed with this path.
const getVCFileName = (str: string) => `${dirname(entrypoint)}/___vc/${str}`;
const launcher = awsLambdaHandler ? makeAwsLauncher : makeVercelLauncher;
const launcherSource = launcher({
entrypointPath: `../${renameTStoJS(basename(entrypoint))}`,
bridgePath: `./${BRIDGE_FILENAME}`,
helpersPath: `./${HELPERS_FILENAME}`,
sourcemapSupportPath: `./${SOURCEMAP_SUPPORT_FILENAME}`,
shouldAddHelpers,
shouldAddSourcemapSupport,
awsLambdaHandler,
});
const launcherFiles: Files = {
[getVCFileName('package.json')]: new FileBlob({
data: JSON.stringify({ type: 'commonjs' }),
}),
[getVCFileName(LAUNCHER_FILENAME)]: new FileBlob({
data: launcherSource,
}),
[getVCFileName(BRIDGE_FILENAME)]: new FileFsRef({
fsPath: join(DIST_DIR, 'bridge.js'),
}),
};
if (shouldAddSourcemapSupport) {
launcherFiles[getVCFileName(SOURCEMAP_SUPPORT_FILENAME)] = new FileFsRef({
fsPath: join(DIST_DIR, 'source-map-support.js'),
});
}
if (shouldAddHelpers) {
launcherFiles[getVCFileName(HELPERS_FILENAME)] = new FileFsRef({
fsPath: join(DIST_DIR, 'helpers.js'),
});
}
// Map `files` to the output workPath
const files = {
...preparedFiles,
...launcherFiles,
};
const nftFiles: { input: string; output: string }[] = [];
for (const filename of Object.keys(files)) {
const outPath = join(outputWorkPath, filename);
const file = files[filename];
await fsp.mkdir(dirname(outPath), { recursive: true });
const ws = createWriteStream(outPath, {
mode: file.mode,
});
const finishPromise = once(ws, 'finish');
file.toStream().pipe(ws);
await finishPromise;
// The `handler` will be `.output/server/pages/api/subdirectory/___vc/__launcher.launcher`
// or `.output/server/pages/api/___vc/__launcher.launcher`.
// This means everything has to be mounted to the `dirname` of the entrypoint.
nftFiles.push({
input: relative(dirname(nftOutput), outPath),
output: join('.output', 'server', 'pages', filename),
});
}
await fsp.writeFile(
nftOutput,
JSON.stringify({
version: 1,
files: nftFiles,
})
);
await fsp.copyFile(
join(outputWorkPath, renameTStoJS(entrypoint)),
pageOutput
);
const pages = {
[normalizePath(relative(pagesDir, pageOutput))]: {
handler: `___vc/${LAUNCHER_FILENAME.slice(0, -3)}.launcher`,
runtime: nodeVersion.runtime,
},
};
await updateFunctionsManifest({ workPath, pages });
// Update the `routes-mainifest.json` file with the wildcard route
// when the entrypoint is dynamic (i.e. `/api/[id].ts`).
if (isDynamicRoute(entrypointWithoutExt)) {
await updateRoutesManifest({
workPath,
dynamicRoutes: [pageToRoute(entrypointWithoutExt)],
});
}
}
export async function prepareCache({
workPath,
}: PrepareCacheOptions): Promise<Files> {
const cache = await glob('node_modules/**', workPath);
return cache;
}
export async function startDevServer(
opts: StartDevServerOptions
): Promise<StartDevServerResult> {
const { entrypoint, workPath, config, meta = {} } = opts;
const entryDir = join(workPath, dirname(entrypoint));
const projectTsConfig = await walkParentDirs({
base: workPath,
start: entryDir,
filename: 'tsconfig.json',
});
const pathToPkg = await walkParentDirs({
base: workPath,
start: entryDir,
filename: 'package.json',
});
const pkg = pathToPkg ? require_(pathToPkg) : {};
const isEsm =
entrypoint.endsWith('.mjs') ||
(pkg.type === 'module' && entrypoint.endsWith('.js'));
const devServerPath = join(DIST_DIR, 'dev-server.js');
const child = fork(devServerPath, [], {
cwd: workPath,
execArgv: [],
env: {
...process.env,
...meta.env,
VERCEL_DEV_ENTRYPOINT: entrypoint,
VERCEL_DEV_TSCONFIG: projectTsConfig || '',
VERCEL_DEV_IS_ESM: isEsm ? '1' : undefined,
VERCEL_DEV_CONFIG: JSON.stringify(config),
VERCEL_DEV_BUILD_ENV: JSON.stringify(meta.buildEnv || {}),
},
});
const { pid } = child;
const controller = new AbortController();
const { signal } = controller;
const onMessage = once(child, 'message', { signal });
const onExit = once(child, 'exit', { signal });
try {
const result = await Promise.race([onMessage, onExit]);
if (isPortInfo(result)) {
// "message" event
const ext = extname(entrypoint);
if (ext === '.ts' || ext === '.tsx') {
// Invoke `tsc --noEmit` asynchronously in the background, so
// that the HTTP request is not blocked by the type checking.
doTypeCheck(opts, projectTsConfig).catch((err: Error) => {
console.error('Type check for %j failed:', entrypoint, err);
});
}
return { port: result.port, pid };
} else {
// Got "exit" event from child process
const [exitCode, signal] = result;
const reason = signal ? `"${signal}" signal` : `exit code ${exitCode}`;
throw new Error(`\`node ${entrypoint}\` failed with ${reason}`);
}
} finally {
controller.abort();
}
}
async function doTypeCheck(
{ entrypoint, workPath, meta = {} }: StartDevServerOptions,
projectTsConfig: string | null
): Promise<void> {
const { devCacheDir = join(workPath, '.now', 'cache') } = meta;
const entrypointCacheDir = join(devCacheDir, 'node', entrypoint);
// In order to type-check a single file, a standalone tsconfig
// file needs to be created that inherits from the base one :(
// See: https://stackoverflow.com/a/44748041/376773
//
// A different filename needs to be used for different `extends` tsconfig.json
const tsconfigName = projectTsConfig
? `tsconfig-with-${relative(workPath, projectTsConfig).replace(
/[\\/.]/g,
'-'
)}.json`
: 'tsconfig.json';
const tsconfigPath = join(entrypointCacheDir, tsconfigName);
const tsconfig = {
extends: projectTsConfig
? relative(entrypointCacheDir, projectTsConfig)
: undefined,
include: [relative(entrypointCacheDir, join(workPath, entrypoint))],
};
try {
const json = JSON.stringify(tsconfig, null, '\t');
await fsp.mkdir(entrypointCacheDir, { recursive: true });
await fsp.writeFile(tsconfigPath, json, { flag: 'wx' });
} catch (err) {
// Don't throw if the file already exists
if (err.code !== 'EEXIST') {
throw err;
}
}
const child = spawn(
process.execPath,
[
tscPath,
'--project',
tsconfigPath,
'--noEmit',
'--allowJs',
'--esModuleInterop',
'--jsx',
'react',
],
{
cwd: workPath,
stdio: 'inherit',
}
);
await once(child, 'exit');
}

View File

@@ -0,0 +1,6 @@
// Identify /[param]/ in route string
const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/;
export function isDynamicRoute(route: string): boolean {
return TEST_ROUTE.test(route);
}

View File

@@ -0,0 +1,16 @@
import { getRouteRegex } from './route-regex';
export function pageToRoute(page: string) {
const routeRegex = getRouteRegex(page);
return {
page,
regex: normalizeRouteRegex(routeRegex.re.source),
routeKeys: routeRegex.routeKeys,
namedRegex: routeRegex.namedRegex,
};
}
export function normalizeRouteRegex(regex: string) {
// clean up un-necessary escaping from regex.source which turns / into \\/
return regex.replace(/\\\//g, '/');
}

View File

@@ -0,0 +1,129 @@
interface Group {
pos: number;
repeat: boolean;
optional: boolean;
}
// this isn't importing the escape-string-regex module
// to reduce bytes
function escapeRegex(str: string) {
return str.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
}
function parseParameter(param: string) {
const optional = param.startsWith('[') && param.endsWith(']');
if (optional) {
param = param.slice(1, -1);
}
const repeat = param.startsWith('...');
if (repeat) {
param = param.slice(3);
}
return { key: param, repeat, optional };
}
export function getParametrizedRoute(route: string) {
const segments = (route.replace(/\/$/, '') || '/').slice(1).split('/');
const groups: { [groupName: string]: Group } = {};
let groupIndex = 1;
const parameterizedRoute = segments
.map(segment => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1));
groups[key] = { pos: groupIndex++, repeat, optional };
return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)';
} else {
return `/${escapeRegex(segment)}`;
}
})
.join('');
// dead code eliminate for browser since it's only needed
// while generating routes-manifest
let routeKeyCharCode = 97;
let routeKeyCharLength = 1;
// builds a minimal routeKey using only a-z and minimal number of characters
const getSafeRouteKey = () => {
let routeKey = '';
for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode);
routeKeyCharCode++;
if (routeKeyCharCode > 122) {
routeKeyCharLength++;
routeKeyCharCode = 97;
}
}
return routeKey;
};
const routeKeys: { [named: string]: string } = {};
const namedParameterizedRoute = segments
.map(segment => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1));
// replace any non-word characters since they can break
// the named regex
let cleanedKey = key.replace(/\W/g, '');
let invalidKey = false;
// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true;
}
if (!isNaN(parseInt(cleanedKey.substr(0, 1)))) {
invalidKey = true;
}
if (invalidKey) {
cleanedKey = getSafeRouteKey();
}
routeKeys[cleanedKey] = key;
return repeat
? optional
? `(?:/(?<${cleanedKey}>.+?))?`
: `/(?<${cleanedKey}>.+?)`
: `/(?<${cleanedKey}>[^/]+?)`;
} else {
return `/${escapeRegex(segment)}`;
}
})
.join('');
return {
parameterizedRoute,
namedParameterizedRoute,
groups,
routeKeys,
};
}
export interface RouteRegex {
groups: { [groupName: string]: Group };
namedRegex?: string;
re: RegExp;
routeKeys?: { [named: string]: string };
}
export function getRouteRegex(normalizedRoute: string): RouteRegex {
const result = getParametrizedRoute(normalizedRoute);
if ('routeKeys' in result) {
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups,
routeKeys: result.routeKeys,
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,
};
}
return {
re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`),
groups: result.groups,
};
}

View File

@@ -0,0 +1,41 @@
import { ServerResponse, IncomingMessage } from 'http';
export type VercelRequestCookies = { [key: string]: string };
export type VercelRequestQuery = { [key: string]: string | string[] };
export type VercelRequestBody = any;
export type VercelRequest = IncomingMessage & {
query: VercelRequestQuery;
cookies: VercelRequestCookies;
body: VercelRequestBody;
};
export type VercelResponse = ServerResponse & {
send: (body: any) => VercelResponse;
json: (jsonBody: any) => VercelResponse;
status: (statusCode: number) => VercelResponse;
redirect: (statusOrUrl: string | number, url?: string) => VercelResponse;
};
export type VercelApiHandler = (
req: VercelRequest,
res: VercelResponse
) => void;
/** @deprecated Use VercelRequestCookies instead. */
export type NowRequestCookies = VercelRequestCookies;
/** @deprecated Use VercelRequestQuery instead. */
export type NowRequestQuery = VercelRequestQuery;
/** @deprecated Use VercelRequestBody instead. */
export type NowRequestBody = any;
/** @deprecated Use VercelRequest instead. */
export type NowRequest = VercelRequest;
/** @deprecated Use VercelResponse instead. */
export type NowResponse = VercelResponse;
/** @deprecated Use VercelApiHandler instead. */
export type NowApiHandler = VercelApiHandler;

View File

@@ -0,0 +1,497 @@
import _ts from 'typescript';
import { NowBuildError } from '@vercel/build-utils';
import { relative, basename, resolve, dirname } from 'path';
/*
* Fork of TS-Node - https://github.com/TypeStrong/ts-node
* Copyright Blake Embrey
* MIT License
*/
/**
* Debugging.
*/
const shouldDebug = false;
const debug = shouldDebug
? console.log.bind(console, 'ts-node')
: () => undefined;
const debugFn = shouldDebug
? <T, U>(key: string, fn: (arg: T) => U) => {
let i = 0;
return (x: T) => {
debug(key, x, ++i);
return fn(x);
};
}
: <T, U>(_: string, fn: (arg: T) => U) => fn;
/**
* Common TypeScript interfaces between versions.
*/
interface TSCommon {
version: typeof _ts.version;
sys: typeof _ts.sys;
ScriptSnapshot: typeof _ts.ScriptSnapshot;
displayPartsToString: typeof _ts.displayPartsToString;
createLanguageService: typeof _ts.createLanguageService;
getDefaultLibFilePath: typeof _ts.getDefaultLibFilePath;
getPreEmitDiagnostics: typeof _ts.getPreEmitDiagnostics;
flattenDiagnosticMessageText: typeof _ts.flattenDiagnosticMessageText;
transpileModule: typeof _ts.transpileModule;
ModuleKind: typeof _ts.ModuleKind;
ScriptTarget: typeof _ts.ScriptTarget;
findConfigFile: typeof _ts.findConfigFile;
readConfigFile: typeof _ts.readConfigFile;
parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent;
formatDiagnostics: typeof _ts.formatDiagnostics;
formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext;
}
/**
* Registration options.
*/
interface Options {
basePath?: string;
pretty?: boolean | null;
logError?: boolean | null;
files?: boolean | null;
compiler?: string;
ignore?: string[];
project?: string;
compilerOptions?: any;
ignoreDiagnostics?: Array<number | string>;
readFile?: (path: string) => string | undefined;
fileExists?: (path: string) => boolean;
transformers?: _ts.CustomTransformers;
}
/**
* Track the project information.
*/
class MemoryCache {
fileContents = new Map<string, string>();
fileVersions = new Map<string, number>();
constructor(rootFileNames: string[] = []) {
for (const fileName of rootFileNames) this.fileVersions.set(fileName, 1);
}
}
/**
* Default register options.
*/
const DEFAULTS: Options = {
files: null,
pretty: null,
compiler: undefined,
compilerOptions: undefined,
ignore: undefined,
project: undefined,
ignoreDiagnostics: undefined,
logError: null,
};
/**
* Default TypeScript compiler options required by `ts-node`.
*/
const TS_NODE_COMPILER_OPTIONS = {
sourceMap: true,
inlineSourceMap: false,
inlineSources: true,
declaration: false,
noEmit: false,
outDir: '$$ts-node$$',
};
/**
* Replace backslashes with forward slashes.
*/
function normalizeSlashes(value: string): string {
return value.replace(/\\/g, '/');
}
/**
* Return type for registering `ts-node`.
*/
export type Register = (
code: string,
fileName: string,
skipTypeCheck?: boolean
) => SourceOutput;
/**
* Cached fs operation wrapper.
*/
function cachedLookup<T>(fn: (arg: string) => T): (arg: string) => T {
const cache = new Map<string, T>();
return (arg: string): T => {
if (!cache.has(arg)) {
cache.set(arg, fn(arg));
}
return cache.get(arg) as T;
};
}
/**
* Register TypeScript compiler.
*/
export function register(opts: Options = {}): Register {
const options = Object.assign({}, DEFAULTS, opts);
const ignoreDiagnostics = [
6059, // "'rootDir' is expected to contain all source files."
18002, // "The 'files' list in config file is empty."
18003, // "No inputs were found in config file."
...(options.ignoreDiagnostics || []),
].map(Number);
// Require the TypeScript compiler and configuration.
const cwd = options.basePath || process.cwd();
const nowNodeBase = resolve(__dirname, '..', '..', '..');
let compiler: string;
const require_ = eval('require');
try {
compiler = require_.resolve(options.compiler || 'typescript', {
paths: [options.project || cwd, nowNodeBase],
});
} catch (e) {
compiler = 'typescript';
}
//eslint-disable-next-line @typescript-eslint/no-var-requires
const ts: typeof _ts = require_(compiler);
if (compiler.startsWith(nowNodeBase)) {
console.log('Using TypeScript ' + ts.version + ' (no local tsconfig.json)');
} else {
console.log('Using TypeScript ' + ts.version + ' (local user-provided)');
}
const transformers = options.transformers || undefined;
const readFile = options.readFile || ts.sys.readFile;
const fileExists = options.fileExists || ts.sys.fileExists;
const formatDiagnostics =
process.stdout.isTTY || options.pretty
? ts.formatDiagnosticsWithColorAndContext
: ts.formatDiagnostics;
const diagnosticHost: _ts.FormatDiagnosticsHost = {
getNewLine: () => ts.sys.newLine,
getCurrentDirectory: () => cwd,
getCanonicalFileName: path => path,
};
function createTSError(diagnostics: ReadonlyArray<_ts.Diagnostic>) {
const message = formatDiagnostics(diagnostics, diagnosticHost);
return new NowBuildError({ code: 'NODE_TYPESCRIPT_ERROR', message });
}
function reportTSError(
diagnostics: _ts.Diagnostic[],
shouldExit: boolean | undefined
) {
if (!diagnostics || diagnostics.length === 0) {
return;
}
const error = createTSError(diagnostics);
if (shouldExit) {
throw error;
} else {
// Print error in red color and continue execution.
console.error('\x1b[31m%s\x1b[0m', error);
}
}
// we create a custom build per tsconfig.json instance
const builds = new Map<string, Build>();
function getBuild(configFileName = ''): Build {
let build = builds.get(configFileName);
if (build) return build;
const config = readConfig(configFileName);
/**
* Create the basic required function using transpile mode.
*/
const getOutput = function (code: string, fileName: string): SourceOutput {
const result = ts.transpileModule(code, {
fileName,
transformers,
compilerOptions: config.options,
reportDiagnostics: true,
});
const diagnosticList = result.diagnostics
? filterDiagnostics(result.diagnostics, ignoreDiagnostics)
: [];
reportTSError(diagnosticList, config.options.noEmitOnError);
return { code: result.outputText, map: result.sourceMapText as string };
};
// Use full language services when the fast option is disabled.
let getOutputTypeCheck: (code: string, fileName: string) => SourceOutput;
{
const memoryCache = new MemoryCache(config.fileNames);
const cachedReadFile = cachedLookup(debugFn('readFile', readFile));
// Create the compiler host for type checking.
const serviceHost: _ts.LanguageServiceHost = {
getScriptFileNames: () => Array.from(memoryCache.fileVersions.keys()),
getScriptVersion: (fileName: string) => {
const version = memoryCache.fileVersions.get(fileName);
return version === undefined ? '' : version.toString();
},
getScriptSnapshot(fileName: string) {
let contents = memoryCache.fileContents.get(fileName);
// Read contents into TypeScript memory cache.
if (contents === undefined) {
contents = cachedReadFile(fileName);
if (contents === undefined) return;
memoryCache.fileVersions.set(fileName, 1);
memoryCache.fileContents.set(fileName, contents);
}
return ts.ScriptSnapshot.fromString(contents);
},
readFile: cachedReadFile,
readDirectory: cachedLookup(
debugFn('readDirectory', ts.sys.readDirectory)
),
getDirectories: cachedLookup(
debugFn('getDirectories', ts.sys.getDirectories)
),
fileExists: cachedLookup(debugFn('fileExists', fileExists)),
directoryExists: cachedLookup(
debugFn('directoryExists', ts.sys.directoryExists)
),
getNewLine: () => ts.sys.newLine,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getCurrentDirectory: () => cwd,
getCompilationSettings: () => config.options,
getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options),
getCustomTransformers: () => transformers,
};
const registry = ts.createDocumentRegistry(
ts.sys.useCaseSensitiveFileNames,
cwd
);
const service = ts.createLanguageService(serviceHost, registry);
// Set the file contents into cache manually.
const updateMemoryCache = function (contents: string, fileName: string) {
const fileVersion = memoryCache.fileVersions.get(fileName) || 0;
// Avoid incrementing cache when nothing has changed.
if (memoryCache.fileContents.get(fileName) === contents) return;
memoryCache.fileVersions.set(fileName, fileVersion + 1);
memoryCache.fileContents.set(fileName, contents);
};
getOutputTypeCheck = function (code: string, fileName: string) {
updateMemoryCache(code, fileName);
const output = service.getEmitOutput(fileName);
// Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`.
const diagnostics = service
.getSemanticDiagnostics(fileName)
.concat(service.getSyntacticDiagnostics(fileName));
const diagnosticList = filterDiagnostics(
diagnostics,
ignoreDiagnostics
);
reportTSError(diagnosticList, config.options.noEmitOnError);
if (output.emitSkipped) {
throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`);
}
// Throw an error when requiring `.d.ts` files.
if (output.outputFiles.length === 0) {
throw new TypeError(
'Unable to require `.d.ts` file.\n' +
'This is usually the result of a faulty configuration or import. ' +
'Make sure there is a `.js`, `.json` or another executable extension and ' +
'loader (attached before `ts-node`) available alongside ' +
`\`${basename(fileName)}\`.`
);
}
return {
code: output.outputFiles[1].text,
map: output.outputFiles[0].text,
};
};
}
builds.set(
configFileName,
(build = {
getOutput,
getOutputTypeCheck,
})
);
return build;
}
// determine the tsconfig.json path for a given folder
function detectConfig(): string | undefined {
let configFileName: string | undefined = undefined;
// Read project configuration when available.
configFileName = options.project
? ts.findConfigFile(normalizeSlashes(options.project), fileExists)
: ts.findConfigFile(normalizeSlashes(cwd), fileExists);
if (configFileName) return normalizeSlashes(configFileName);
}
/**
* Load TypeScript configuration.
*/
function readConfig(configFileName: string): _ts.ParsedCommandLine {
let config: any = { compilerOptions: {} };
const basePath = normalizeSlashes(dirname(configFileName));
// Read project configuration when available.
if (configFileName) {
const result = ts.readConfigFile(configFileName, readFile);
// Return diagnostics.
if (result.error) {
const errorResult = {
errors: [result.error],
fileNames: [],
options: {},
};
const configDiagnosticList = filterDiagnostics(
errorResult.errors,
ignoreDiagnostics
);
// Render the configuration errors.
reportTSError(configDiagnosticList, true);
return errorResult;
}
config = result.config;
}
// Remove resolution of "files".
if (!options.files) {
config.files = [];
config.include = [];
}
// Override default configuration options `ts-node` requires.
config.compilerOptions = Object.assign(
{},
config.compilerOptions,
options.compilerOptions,
TS_NODE_COMPILER_OPTIONS
);
const configResult = fixConfig(
ts,
ts.parseJsonConfigFileContent(
config,
ts.sys,
basePath,
undefined,
configFileName
)
);
if (configFileName) {
const configDiagnosticList = filterDiagnostics(
configResult.errors,
ignoreDiagnostics
);
// Render the configuration errors.
reportTSError(configDiagnosticList, configResult.options.noEmitOnError);
}
return configResult;
}
// Create a simple TypeScript compiler proxy.
function compile(
code: string,
fileName: string,
skipTypeCheck?: boolean
): SourceOutput {
const configFileName = detectConfig();
const build = getBuild(configFileName);
const { code: value, map: sourceMap } = (
skipTypeCheck ? build.getOutput : build.getOutputTypeCheck
)(code, fileName);
const output = {
code: value,
map: Object.assign(JSON.parse(sourceMap), {
file: basename(fileName),
sources: [fileName],
}),
};
delete output.map.sourceRoot;
return output;
}
return compile;
}
interface Build {
getOutput(code: string, fileName: string): SourceOutput;
getOutputTypeCheck(code: string, fileName: string): SourceOutput;
}
/**
* Do post-processing on config options to support `ts-node`.
*/
function fixConfig(ts: TSCommon, config: _ts.ParsedCommandLine) {
// Delete options that *should not* be passed through.
delete config.options.out;
delete config.options.outFile;
delete config.options.composite;
delete config.options.declarationDir;
delete config.options.declarationMap;
delete config.options.emitDeclarationOnly;
delete config.options.tsBuildInfoFile;
delete config.options.incremental;
// Target esnext output by default (instead of ES3).
// This will prevent TS from polyfill/downlevel emit.
if (config.options.target === undefined) {
config.options.target = ts.ScriptTarget.ESNext;
}
// When mixing TS with JS, its best to enable this flag.
// This is useful when no `tsconfig.json` is supplied.
if (config.options.esModuleInterop === undefined) {
config.options.esModuleInterop = true;
}
// Target CommonJS, always!
config.options.module = ts.ModuleKind.CommonJS;
return config;
}
/**
* Internal source output.
*/
type SourceOutput = { code: string; map: string };
/**
* Filter diagnostics.
*/
function filterDiagnostics(diagnostics: _ts.Diagnostic[], ignore: number[]) {
return diagnostics.filter(x => ignore.indexOf(x.code) === -1);
}

377
packages/plugin-node/test/build.test.ts vendored Normal file
View File

@@ -0,0 +1,377 @@
import path from 'path';
import { parse } from 'url';
import { promises as fsp } from 'fs';
import { ZipFile } from 'yazl';
import { createFunction, Lambda } from '@vercel/fun';
import {
Request,
HeadersInit,
RequestInfo,
RequestInit,
Response,
Headers,
} from 'node-fetch';
import { build } from '../src';
import { runNpmInstall, streamToBuffer } from '@vercel/build-utils';
interface TestParams {
fixture: string;
fetch: (r: RequestInfo, init?: RequestInit) => Promise<Response>;
}
interface VercelResponsePayload {
statusCode: number;
headers: { [name: string]: string };
encoding?: 'base64';
body: string;
}
function headersToObject(headers: Headers) {
const h: { [name: string]: string } = {};
for (const [name, value] of headers) {
h[name] = value;
}
return h;
}
function toBase64(body?: Buffer | NodeJS.ReadableStream) {
if (!body) return undefined;
if (Buffer.isBuffer(body)) {
return body.toString('base64');
}
return new Promise<string>((res, rej) => {
const buffers: Buffer[] = [];
body.on('data', b => buffers.push(b));
body.on('end', () => res(Buffer.concat(buffers).toString('base64')));
body.on('error', rej);
});
}
function withFixture<T>(
name: string,
t: (props: TestParams) => Promise<T>
): () => Promise<T> {
return async () => {
const fixture = path.join(__dirname, 'fixtures', name);
await fsp.rmdir(path.join(fixture, '.output'), { recursive: true });
const functions = new Map<string, Lambda>();
async function fetch(r: RequestInfo, init?: RequestInit) {
const req = new Request(r, init);
const url = parse(req.url);
const functionPath = url.pathname!.substring(1);
let status = 404;
let headers: HeadersInit = {};
let body: string | Buffer = 'Function not found';
let fn = functions.get(functionPath);
if (!fn) {
const manifest = JSON.parse(
await fsp.readFile(
path.join(fixture, '.output/functions-manifest.json'),
'utf8'
)
);
const keyFile = `${functionPath}.js`;
const keyIndex = `${functionPath}/index.js`;
const fnKey = keyFile in manifest.pages ? keyFile : keyIndex;
const functionManifest = manifest.pages[fnKey];
if (functionManifest) {
const entry = path.join(fixture, '.output/server/pages', fnKey);
const nftFile = JSON.parse(
await fsp.readFile(`${entry}.nft.json`, 'utf8')
);
const zip = new ZipFile();
zip.addFile(
path.join(fixture, '.output/server/pages', fnKey),
path.join('.output/server/pages', fnKey)
);
nftFile.files.forEach((f: { input: string; output: string }) => {
const input = path.join(path.dirname(entry), f.input);
zip.addFile(input, f.output);
});
zip.end();
const handler = path.posix.join(
'.output/server/pages',
path.dirname(fnKey),
functionManifest.handler
);
fn = await createFunction({
Code: {
ZipFile: await streamToBuffer(zip.outputStream),
},
Handler: handler,
Runtime: functionManifest.runtime,
});
functions.set(functionPath, fn);
}
}
if (fn) {
const payload: VercelResponsePayload = await fn({
Action: 'Invoke',
body: JSON.stringify({
method: req.method,
path: req.url,
headers: headersToObject(req.headers),
body: await toBase64(req.body),
encoding: 'base64',
}),
});
status = payload.statusCode;
headers = payload.headers;
body = Buffer.from(payload.body, payload.encoding || 'utf8');
}
return new Response(body, {
status,
headers,
});
}
if (
await fsp.lstat(path.join(fixture, 'package.json')).catch(() => false)
) {
await runNpmInstall(fixture);
}
await build({ workPath: fixture });
try {
return await t({ fixture, fetch });
} finally {
await Promise.all(Array.from(functions.values()).map(f => f.destroy()));
}
};
}
describe('build()', () => {
// Longer timeout to install deps of fixtures
jest.setTimeout(60 * 1000);
// Basic test with no dependencies
// Also tests `req.query`
it(
'should build "hello"',
withFixture('hello', async ({ fetch }) => {
const res = await fetch('/api/hello');
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toEqual('Hello world!');
const res2 = await fetch('/api/hello?place=SF');
expect(res2.status).toEqual(200);
const body2 = await res2.text();
expect(body2).toEqual('Hello SF!');
})
);
// Tests a basic dependency with root-level `package.json`
// and an endpoint in a subdirectory with its own `package.json`
it(
'should build "cowsay"',
withFixture('cowsay', async ({ fetch }) => {
const res = await fetch('/api');
expect(res.status).toEqual(200);
const body = await res.text();
expect(body).toEqual(
' ____________________________\n' +
'< cow:RANDOMNESS_PLACEHOLDER >\n' +
' ----------------------------\n' +
' \\ ^__^\n' +
' \\ (oo)\\_______\n' +
' (__)\\ )\\/\\\n' +
' ||----w |\n' +
' || ||'
);
const res2 = await fetch('/api/subdirectory');
expect(res2.status).toEqual(200);
const body2 = await res2.text();
expect(body2).toEqual(
' _____________________________\n' +
'< yoda:RANDOMNESS_PLACEHOLDER >\n' +
' -----------------------------\n' +
' \\\n' +
' \\\n' +
' .--.\n' +
" \\`--._,'.::.`._.--'/\n" +
" . ` __::__ ' .\n" +
" -:.`'..`'.:-\n" +
" \\ `--' /\n" +
' ----\n'
);
})
);
// Tests the legacy Node.js server interface where
// `server.listen()` is explicitly called
it(
'should build "node-server"',
withFixture('node-server', async ({ fetch }) => {
const res = await fetch('/api');
expect(await res.text()).toEqual('root');
const res2 = await fetch('/api/subdirectory');
expect(await res2.text()).toEqual('subdir');
const res3 = await fetch('/api/hapi-async');
expect(await res3.text()).toEqual('hapi-async');
})
);
// Tests the importing a `.tsx` file
it(
'should build "tsx-resolve"',
withFixture('tsx-resolve', async ({ fetch }) => {
const res = await fetch('/api');
const body = await res.text();
expect(body).toEqual('tsx');
})
);
// Tests that nft includes statically detected asset files
it(
'should build "assets"',
withFixture('assets', async ({ fetch }) => {
const res = await fetch('/api');
const body = await res.text();
expect(body).toEqual('asset1,asset2');
})
);
// Tests the `includeFiles` config option
it(
'should build "include-files"',
withFixture('include-files', async ({ fetch }) => {
const res = await fetch('/api');
const body = await res.text();
expect(body.includes('hello Vercel!')).toEqual(true);
const res2 = await fetch('/api/include-ts-file');
const body2 = await res2.text();
expect(body2.includes("const foo = 'hello TS!'")).toEqual(true);
const res3 = await fetch('/api/root');
const body3 = await res3.text();
expect(body3.includes('hello Root!')).toEqual(true);
const res4 = await fetch('/api/accepts-string');
const body4 = await res4.text();
expect(body4.includes('hello String!')).toEqual(true);
})
);
// Tests the Vercel helper properties / functions
it(
'should build "helpers"',
withFixture('helpers', async ({ fetch }) => {
const res = await fetch('/api');
const body = await res.text();
expect(body).toEqual('hello anonymous');
const res2 = await fetch('/api?who=bill');
const body2 = await res2.text();
expect(body2).toEqual('hello bill');
const res3 = await fetch('/api', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ who: 'john' }),
});
const body3 = await res3.text();
expect(body3).toEqual('hello john');
const res4 = await fetch('/api', {
headers: { cookie: 'who=chris' },
});
const body4 = await res4.text();
expect(body4).toEqual('hello chris');
const res5 = await fetch('/api/ts');
expect(res5.status).toEqual(404);
const body5 = await res5.text();
expect(body5).toEqual('not found');
const res6 = await fetch('/api/micro-compat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ who: 'katie' }),
});
const body6 = await res6.text();
expect(body6).toEqual('hello katie');
const res7 = await fetch('/api/no-helpers');
const body7 = await res7.text();
expect(body7).toEqual('no');
})
);
// Tests the `awsHandlerName` config option
it(
'should build "aws-api"',
withFixture('aws-api', async ({ fetch }) => {
const res = await fetch('/api');
const body = await res.text();
expect(body).toEqual(
' ______________\n' +
'< aws-api-root >\n' +
' --------------\n' +
' \\ ^__^\n' +
' \\ (oo)\\_______\n' +
' (__)\\ )\\/\\\n' +
' ||----w |\n' +
' || ||'
);
const res2 = await fetch('/api/callback');
const body2 = await res2.text();
expect(body2).toEqual(
' __________________\n' +
'< aws-api-callback >\n' +
' ------------------\n' +
' \\ ^__^\n' +
' \\ (oo)\\_______\n' +
' (__)\\ )\\/\\\n' +
' ||----w |\n' +
' || ||'
);
const res3 = await fetch('/api/graphql');
const body3 = await res3.text();
expect(body3.includes('GraphQL Playground')).toEqual(true);
})
);
it(
'should build "nested-lock-and-build"',
withFixture('nested-lock-and-build', async ({ fetch }) => {
const resA = await fetch('/api/users/[id]');
expect(resA.headers.get('x-date')).toEqual('2021-11-20T20:00:00.000Z');
const body = await resA.text();
expect(body).toEqual(
' _______________________________\n' +
'< Hello from /api/users/[id].js >\n' +
' -------------------------------\n' +
' \\ ^__^\n' +
' \\ (oo)\\_______\n' +
' (__)\\ )\\/\\\n' +
' ||----w |\n' +
' || ||'
);
const resB = await fetch('/api/profile');
expect(await resB.text()).toEqual('true');
})
);
});

View File

@@ -0,0 +1,7 @@
const assert = require('assert');
module.exports = (req, resp) => {
assert(!process.env.RANDOMNESS_BUILD_ENV_VAR);
assert(process.env.RANDOMNESS_ENV_VAR);
resp.end('BUILD_TIME_PLACEHOLDER:build-env');
};

View File

@@ -0,0 +1,12 @@
const assert = require('assert');
const fs = require('fs');
assert(process.env.RANDOMNESS_BUILD_ENV_VAR);
assert(!process.env.RANDOMNESS_ENV_VAR);
fs.writeFileSync(
'index.js',
fs
.readFileSync('index.js', 'utf8')
.replace('BUILD_TIME_PLACEHOLDER', process.env.RANDOMNESS_BUILD_ENV_VAR)
);

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"now-build": "node now-build.js"
}
}

View File

@@ -0,0 +1,7 @@
const assert = require('assert');
module.exports = (req, resp) => {
assert(!process.env.RANDOMNESS_BUILD_ENV_VAR);
assert(process.env.RANDOMNESS_ENV_VAR);
resp.end(`${process.env.RANDOMNESS_ENV_VAR}:env`);
};

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [
{ "src": "build-env/index.js", "use": "@vercel/node" },
{ "src": "env/index.js", "use": "@vercel/node" }
],
"probes": [
{ "path": "/build-env", "mustContain": "RANDOMNESS_PLACEHOLDER:build-env" },
{ "path": "/env", "mustContain": "RANDOMNESS_PLACEHOLDER:env" }
]
}

View File

@@ -0,0 +1,13 @@
const { strictEqual } = require('assert');
async function test3({ deploymentUrl, fetch, randomness }) {
const bodyMustBe = `${randomness}:content-length`;
const resp = await fetch(`https://${deploymentUrl}/test3.js`);
strictEqual(resp.status, 401);
strictEqual(await resp.text(), bodyMustBe);
strictEqual(resp.headers.get('content-length'), String(bodyMustBe.length));
}
module.exports = async ({ deploymentUrl, fetch, randomness }) => {
await test3({ deploymentUrl, fetch, randomness });
};

View File

@@ -0,0 +1,4 @@
module.exports = (_, resp) => {
resp.writeHead(401);
resp.end(`${process.env.RANDOMNESS_ENV_VAR}:content-length`);
};

View File

@@ -0,0 +1,8 @@
{
"version": 2,
"builds": [
{ "src": "test1.js", "use": "@vercel/node" },
{ "src": "test2.js", "use": "@vercel/node" },
{ "src": "test3.js", "use": "@vercel/node" }
]
}

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

@@ -0,0 +1,3 @@
{
"name": "missing-engines-key-on-purpose"
}

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