mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 21:07:47 +00:00
Compare commits
3 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04029013a6 | ||
|
|
c65e7fa883 | ||
|
|
27b68be93f |
@@ -14,4 +14,6 @@ packages/client/lib
|
||||
packages/node/src/bridge.ts
|
||||
packages/node-bridge/bridge.js
|
||||
packages/node-bridge/launcher.js
|
||||
packages/node-bridge/helpers.js
|
||||
packages/node-bridge/source-map-support.js
|
||||
packages/middleware/src/entries.js
|
||||
|
||||
3
examples/sanity/.env.template
Normal file
3
examples/sanity/.env.template
Normal file
@@ -0,0 +1,3 @@
|
||||
# Run `vercel env pull` to generate a .env file from your Vercel project
|
||||
SANITY_STUDIO_API_PROJECT_ID=
|
||||
SANITY_STUDIO_API_DATASET=
|
||||
12
examples/sanity/.npmignore
Normal file
12
examples/sanity/.npmignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
/coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Compiled sanity studio
|
||||
/dist
|
||||
29
examples/sanity/README.md
Normal file
29
examples/sanity/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Sanity Blogging Content Studio
|
||||
|
||||
Congratulations, you have now installed Sanity Studio, an open source real-time content editing environment connected to the Sanity backend.
|
||||
|
||||
Now you can do the following things:
|
||||
|
||||
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
|
||||
- Check out the example frontend: [React/Next.js](https://github.com/sanity-io/tutorial-sanity-blog-react-next)
|
||||
- [Read the blog post about this template](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=readme)
|
||||
- [Join the community Slack](https://slack.sanity.io/?utm_source=readme)
|
||||
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
|
||||
|
||||
## Develop locally
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
npx @sanity/cli install
|
||||
```
|
||||
|
||||
Pull down environment variables from your Vercel project (requires the [Vercel CLI](https://vercel.com/cli)):
|
||||
|
||||
```sh
|
||||
vercel env pull
|
||||
```
|
||||
|
||||
|
||||
|
||||
You can also run `npx @sanity/init` in this repo and agree to reconfigure it. You'll then be able to select from existing projects. The CLI will update `sanity.json` with the project ID and dataset name.
|
||||
7
examples/sanity/config/.checksums
Normal file
7
examples/sanity/config/.checksums
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!",
|
||||
"@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea",
|
||||
"@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f",
|
||||
"@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa",
|
||||
"@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6"
|
||||
}
|
||||
3
examples/sanity/config/@sanity/data-aspects.json
Normal file
3
examples/sanity/config/@sanity/data-aspects.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"listOptions": {}
|
||||
}
|
||||
6
examples/sanity/config/@sanity/default-layout.json
Normal file
6
examples/sanity/config/@sanity/default-layout.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"toolSwitcher": {
|
||||
"order": [],
|
||||
"hidden": []
|
||||
}
|
||||
}
|
||||
7
examples/sanity/config/@sanity/default-login.json
Normal file
7
examples/sanity/config/@sanity/default-login.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"providers": {
|
||||
"mode": "append",
|
||||
"redirectOnSingle": false,
|
||||
"entries": []
|
||||
}
|
||||
}
|
||||
5
examples/sanity/config/@sanity/form-builder.json
Normal file
5
examples/sanity/config/@sanity/form-builder.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"images": {
|
||||
"directUploads": true
|
||||
}
|
||||
}
|
||||
30
examples/sanity/package.json
Normal file
30
examples/sanity/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "verceltemplateblogstudio",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "This is the public list of examples for **Vercel**",
|
||||
"main": "package.json",
|
||||
"author": "Knut Melvær <knut@sanity.io>",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"start": "sanity start",
|
||||
"build": "sanity build"
|
||||
},
|
||||
"keywords": [
|
||||
"sanity"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sanity/core": "^2.26",
|
||||
"@sanity/default-layout": "^2.26",
|
||||
"@sanity/default-login": "^2.26",
|
||||
"@sanity/desk-tool": "^2.26",
|
||||
"@sanity/vision": "^2.26",
|
||||
"prop-types": "^15.7",
|
||||
"react": "^17.0",
|
||||
"react-dom": "^17.0",
|
||||
"styled-components": "^5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sanity/cli": "^2.26"
|
||||
}
|
||||
}
|
||||
1
examples/sanity/plugins/.gitkeep
Normal file
1
examples/sanity/plugins/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
User-specific packages can be placed here
|
||||
29
examples/sanity/sanity.json
Normal file
29
examples/sanity/sanity.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"root": true,
|
||||
"project": {
|
||||
"name": "vercel-template-blog-studio"
|
||||
},
|
||||
"api": {
|
||||
"projectId": "YOUR_PROJECT_ID",
|
||||
"dataset": "YOUR_DATASET_NAME"
|
||||
},
|
||||
"plugins": [
|
||||
"@sanity/base",
|
||||
"@sanity/default-layout",
|
||||
"@sanity/default-login",
|
||||
"@sanity/desk-tool"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"@sanity/vision"
|
||||
]
|
||||
}
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"name": "part:@sanity/base/schema",
|
||||
"path": "./schemas/schema"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
examples/sanity/schemas/author.js
Normal file
48
examples/sanity/schemas/author.js
Normal file
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
name: 'author',
|
||||
title: 'Author',
|
||||
type: 'document',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
options: {
|
||||
source: 'name',
|
||||
maxLength: 96,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
title: 'Image',
|
||||
type: 'image',
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'bio',
|
||||
title: 'Bio',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
title: 'Block',
|
||||
type: 'block',
|
||||
styles: [{title: 'Normal', value: 'normal'}],
|
||||
lists: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
media: 'image',
|
||||
},
|
||||
},
|
||||
}
|
||||
65
examples/sanity/schemas/blockContent.js
Normal file
65
examples/sanity/schemas/blockContent.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* This is the schema definition for the rich text fields used for
|
||||
* for this blog studio. When you import it in schemas.js it can be
|
||||
* reused in other parts of the studio with:
|
||||
* {
|
||||
* name: 'someName',
|
||||
* title: 'Some title',
|
||||
* type: 'blockContent'
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
title: 'Block Content',
|
||||
name: 'blockContent',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
title: 'Block',
|
||||
type: 'block',
|
||||
// Styles let you set what your user can mark up blocks with. These
|
||||
// correspond with HTML tags, but you can set any title or value
|
||||
// you want and decide how you want to deal with it where you want to
|
||||
// use your content.
|
||||
styles: [
|
||||
{title: 'Normal', value: 'normal'},
|
||||
{title: 'H1', value: 'h1'},
|
||||
{title: 'H2', value: 'h2'},
|
||||
{title: 'H3', value: 'h3'},
|
||||
{title: 'H4', value: 'h4'},
|
||||
{title: 'Quote', value: 'blockquote'},
|
||||
],
|
||||
lists: [{title: 'Bullet', value: 'bullet'}],
|
||||
// Marks let you mark up inline text in the block editor.
|
||||
marks: {
|
||||
// Decorators usually describe a single property – e.g. a typographic
|
||||
// preference or highlighting by editors.
|
||||
decorators: [
|
||||
{title: 'Strong', value: 'strong'},
|
||||
{title: 'Emphasis', value: 'em'},
|
||||
],
|
||||
// Annotations can be any object structure – e.g. a link or a footnote.
|
||||
annotations: [
|
||||
{
|
||||
title: 'URL',
|
||||
name: 'link',
|
||||
type: 'object',
|
||||
fields: [
|
||||
{
|
||||
title: 'URL',
|
||||
name: 'href',
|
||||
type: 'url',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// You can add additional types here. Note that you can't use
|
||||
// primitive types such as 'string' and 'number' in the same array
|
||||
// as a block type.
|
||||
{
|
||||
type: 'image',
|
||||
options: {hotspot: true},
|
||||
},
|
||||
],
|
||||
}
|
||||
17
examples/sanity/schemas/category.js
Normal file
17
examples/sanity/schemas/category.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
name: 'category',
|
||||
title: 'Category',
|
||||
type: 'document',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
65
examples/sanity/schemas/post.js
Normal file
65
examples/sanity/schemas/post.js
Normal file
@@ -0,0 +1,65 @@
|
||||
export default {
|
||||
name: 'post',
|
||||
title: 'Post',
|
||||
type: 'document',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
title: 'Title',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'slug',
|
||||
options: {
|
||||
source: 'title',
|
||||
maxLength: 96,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
title: 'Author',
|
||||
type: 'reference',
|
||||
to: {type: 'author'},
|
||||
},
|
||||
{
|
||||
name: 'mainImage',
|
||||
title: 'Main image',
|
||||
type: 'image',
|
||||
options: {
|
||||
hotspot: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
title: 'Categories',
|
||||
type: 'array',
|
||||
of: [{type: 'reference', to: {type: 'category'}}],
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
title: 'Published at',
|
||||
type: 'datetime',
|
||||
},
|
||||
{
|
||||
name: 'body',
|
||||
title: 'Body',
|
||||
type: 'blockContent',
|
||||
},
|
||||
],
|
||||
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
author: 'author.name',
|
||||
media: 'mainImage',
|
||||
},
|
||||
prepare(selection) {
|
||||
const {author} = selection
|
||||
return Object.assign({}, selection, {
|
||||
subtitle: author && `by ${author}`,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
29
examples/sanity/schemas/schema.js
Normal file
29
examples/sanity/schemas/schema.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// First, we must import the schema creator
|
||||
import createSchema from 'part:@sanity/base/schema-creator'
|
||||
|
||||
// Then import schema types from any plugins that might expose them
|
||||
import schemaTypes from 'all:part:@sanity/base/schema-type'
|
||||
|
||||
// We import object and document schemas
|
||||
import blockContent from './blockContent'
|
||||
import category from './category'
|
||||
import post from './post'
|
||||
import author from './author'
|
||||
|
||||
// Then we give our schema to the builder and provide the result to Sanity
|
||||
export default createSchema({
|
||||
// We name our schema
|
||||
name: 'default',
|
||||
// Then proceed to concatenate our document type
|
||||
// to the ones provided by any plugins that are installed
|
||||
types: schemaTypes.concat([
|
||||
// The following are document types which will appear
|
||||
// in the studio.
|
||||
post,
|
||||
author,
|
||||
category,
|
||||
// When added to this list, object types can be used as
|
||||
// { type: 'typename' } in other document schemas
|
||||
blockContent,
|
||||
]),
|
||||
})
|
||||
1
examples/sanity/static/.gitkeep
Normal file
1
examples/sanity/static/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
Files placed here will be served by the Sanity server under the `/static`-prefix
|
||||
BIN
examples/sanity/static/favicon.ico
Normal file
BIN
examples/sanity/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
6
examples/sanity/tsconfig.json
Normal file
6
examples/sanity/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
// Note: This config is only used to help editors like VS Code understand/resolve
|
||||
// parts, the actual transpilation is done by babel. Any compiler configuration in
|
||||
// here will be ignored.
|
||||
"include": ["./node_modules/@sanity/base/types/**/*.ts", "./**/*.ts", "./**/*.tsx"]
|
||||
}
|
||||
9911
examples/sanity/yarn.lock
Normal file
9911
examples/sanity/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.14.0",
|
||||
"version": "2.14.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@types/node-fetch": "^2.1.6",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "^2.4.1",
|
||||
"@vercel/frameworks": "0.6.0",
|
||||
"@vercel/frameworks": "0.6.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"async-retry": "1.2.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "24.0.0",
|
||||
"version": "24.0.1-canary.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -43,11 +43,11 @@
|
||||
"node": ">= 12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/go": "1.3.0",
|
||||
"@vercel/node": "1.13.0",
|
||||
"@vercel/python": "2.2.0",
|
||||
"@vercel/ruby": "1.3.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@vercel/go": "1.3.1-canary.0",
|
||||
"@vercel/node": "1.13.1-canary.0",
|
||||
"@vercel/python": "2.2.1-canary.0",
|
||||
"@vercel/ruby": "1.3.1-canary.0",
|
||||
"update-notifier": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -88,9 +88,9 @@
|
||||
"@types/update-notifier": "5.1.0",
|
||||
"@types/which": "1.3.2",
|
||||
"@types/write-json-file": "2.2.1",
|
||||
"@vercel/client": "10.3.0",
|
||||
"@vercel/client": "10.3.1-canary.0",
|
||||
"@vercel/fetch-retry": "5.0.3",
|
||||
"@vercel/frameworks": "0.6.0",
|
||||
"@vercel/frameworks": "0.6.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.5",
|
||||
"@zeit/fun": "0.11.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "10.3.0",
|
||||
"version": "10.3.1-canary.0",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -40,7 +40,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
6
packages/frameworks/logos/sanity.svg
Normal file
6
packages/frameworks/logos/sanity.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128" height="128" width="128">
|
||||
<rect fill="#F03E2F" height="128" width="128"></rect>
|
||||
<path fill="white" d="M39.4229 33.1629C39.4229 44.1614 46.3362 50.7055 60.1767 54.1563L74.8429 57.4971C87.9417 60.453 95.9185 67.7945 95.9185 79.7554C96.0204 84.9662 94.296 90.053 91.0345 94.1634C91.0345 82.23 84.751 75.7822 69.595 71.9052L55.1947 68.6881C43.6633 66.1035 34.7628 60.068 34.7628 47.076C34.7021 42.0589 36.3415 37.1644 39.4229 33.1629Z"></path>
|
||||
<path fill="#F9B1AB" d="M82.0221 76.827C88.2776 80.759 91.0205 86.2583 91.0205 94.1497C85.8426 100.666 76.7462 104.323 66.0545 104.323C48.0576 104.323 35.4626 95.6207 32.6637 80.4978H49.9468C52.172 87.4406 58.0636 90.6577 65.9285 90.6577C75.5287 90.6577 81.9102 85.6258 82.0361 76.7995"></path>
|
||||
<path fill="#F9B1AB" d="M48.4074 49.4682C45.5509 47.8004 43.2073 45.404 41.6255 42.5332C40.0437 39.6624 39.2825 36.4244 39.423 33.1629C44.419 26.7013 53.1095 22.7556 63.7033 22.7556C82.0361 22.7556 92.6439 32.2693 95.2608 45.66H78.6354C76.8021 40.3807 72.212 36.27 63.8433 36.27C54.9008 36.27 48.7992 41.3843 48.4494 49.4682"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/frameworks",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1-canary.0",
|
||||
"main": "./dist/frameworks.js",
|
||||
"types": "./dist/frameworks.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -2006,6 +2006,50 @@ export const frameworks = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Sanity',
|
||||
slug: 'sanity',
|
||||
demo: 'https://sanity-studio-template.vercel.app',
|
||||
logo: 'https://raw.githubusercontent.com/vercel/vercel/main/packages/frameworks/logos/sanity.svg',
|
||||
tagline:
|
||||
'The structured content platform.',
|
||||
description: 'A Sanity Studio',
|
||||
website: 'https://www.sanity.io',
|
||||
envPrefix: 'SANITY_STUDIO_',
|
||||
detectors: {
|
||||
every: [
|
||||
{
|
||||
path: 'sanity.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install` or `npm install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `sanity build`',
|
||||
value: 'sanity build',
|
||||
},
|
||||
devCommand: {
|
||||
value: 'sanity start --port $PORT',
|
||||
},
|
||||
outputDirectory: {
|
||||
value: 'dist',
|
||||
},
|
||||
},
|
||||
dependency: '@sanity/cli',
|
||||
getOutputDirName: async () => 'dist',
|
||||
defaultRoutes: [
|
||||
{
|
||||
handle: 'filesystem',
|
||||
},
|
||||
{
|
||||
src: '/(.*)',
|
||||
dest: '/index.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
slug: null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/go",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/fs-extra": "^5.0.5",
|
||||
"@types/node-fetch": "^2.3.0",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"async-retry": "1.3.1",
|
||||
"execa": "^1.0.0",
|
||||
|
||||
2
packages/node-bridge/.gitignore
vendored
Normal file
2
packages/node-bridge/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/helpers.js
|
||||
/source-map-support.js
|
||||
62
packages/node-bridge/build.js
Normal file
62
packages/node-bridge/build.js
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs-extra');
|
||||
const execa = require('execa');
|
||||
const { join } = require('path');
|
||||
|
||||
async function main() {
|
||||
// Build TypeScript files
|
||||
await execa('tsc', [], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// Bundle `helpers.ts` with ncc
|
||||
await fs.remove(join(__dirname, 'helpers.js'));
|
||||
const helpersDir = join(__dirname, 'helpers');
|
||||
await execa(
|
||||
'ncc',
|
||||
[
|
||||
'build',
|
||||
join(__dirname, 'helpers.ts'),
|
||||
'-e',
|
||||
'@vercel/node-bridge',
|
||||
'-e',
|
||||
'@vercel/build-utils',
|
||||
'-e',
|
||||
'typescript',
|
||||
'-o',
|
||||
helpersDir,
|
||||
],
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
await fs.rename(join(helpersDir, 'index.js'), join(__dirname, 'helpers.js'));
|
||||
await fs.remove(helpersDir);
|
||||
|
||||
// Bundle `source-map-support/register` with ncc for source maps
|
||||
const sourceMapSupportDir = join(__dirname, '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(__dirname, 'source-map-support.js')
|
||||
);
|
||||
await fs.remove(sourceMapSupportDir);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
309
packages/node-bridge/helpers.ts
Normal file
309
packages/node-bridge/helpers.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type {
|
||||
VercelRequest,
|
||||
VercelResponse,
|
||||
VercelRequestCookies,
|
||||
VercelRequestQuery,
|
||||
VercelRequestBody,
|
||||
} from './types';
|
||||
import { Server } from 'http';
|
||||
import type { Bridge } from './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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node-bridge",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./index.js",
|
||||
"repository": {
|
||||
@@ -11,10 +11,12 @@
|
||||
"files": [
|
||||
"bridge.*",
|
||||
"launcher.*",
|
||||
"index.js"
|
||||
"index.js",
|
||||
"helpers.js",
|
||||
"source-map-support.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "node build.js",
|
||||
"test-unit": "jest --env node --verbose --runInBand --bail"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
842
packages/node-bridge/test/helpers.test.js
vendored
Normal file
842
packages/node-bridge/test/helpers.test.js
vendored
Normal file
@@ -0,0 +1,842 @@
|
||||
const fetch = require('node-fetch');
|
||||
const listen = require('test-listen');
|
||||
const qs = require('querystring');
|
||||
|
||||
const { createServerWithHelpers } = require('../helpers');
|
||||
|
||||
const mockListener = jest.fn();
|
||||
const consumeEventMock = jest.fn();
|
||||
const mockBridge = { consumeEvent: consumeEventMock };
|
||||
|
||||
let server;
|
||||
let url;
|
||||
|
||||
async function fetchWithProxyReq(_url, opts = {}) {
|
||||
if (opts.body) {
|
||||
// eslint-disable-next-line
|
||||
opts = { ...opts, body: Buffer.from(opts.body) };
|
||||
}
|
||||
|
||||
consumeEventMock.mockImplementationOnce(() => opts);
|
||||
|
||||
return fetch(_url, {
|
||||
...opts,
|
||||
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' },
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockListener.mockClear();
|
||||
consumeEventMock.mockClear();
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('hello');
|
||||
});
|
||||
consumeEventMock.mockImplementation(() => ({}));
|
||||
|
||||
server = createServerWithHelpers(mockListener, mockBridge);
|
||||
url = await listen(server);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
describe('contract with @vercel/node-bridge', () => {
|
||||
test('should call consumeEvent with the correct reqId', async () => {
|
||||
await fetchWithProxyReq(`${url}/`);
|
||||
|
||||
expect(consumeEventMock).toHaveBeenLastCalledWith('2');
|
||||
});
|
||||
|
||||
test('should not expose the request id header', async () => {
|
||||
await fetchWithProxyReq(`${url}/`, { headers: { 'x-test-header': 'ok' } });
|
||||
|
||||
const [{ headers }] = mockListener.mock.calls[0];
|
||||
|
||||
expect(headers['x-now-bridge-request-id']).toBeUndefined();
|
||||
expect(headers['x-test-header']).toBe('ok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('all helpers', () => {
|
||||
const nowHelpers = [
|
||||
['query', 0],
|
||||
['cookies', 0],
|
||||
['body', 0],
|
||||
['status', 1],
|
||||
['redirect', 1],
|
||||
['send', 1],
|
||||
['json', 1],
|
||||
];
|
||||
|
||||
test('should not recalculate req properties twice', async () => {
|
||||
const spy = jest.fn(() => {});
|
||||
|
||||
const nowReqHelpers = nowHelpers.filter(([, i]) => i === 0);
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
spy(...nowReqHelpers.map(h => req[h]));
|
||||
spy(...nowReqHelpers.map(h => req[h]));
|
||||
res.end();
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(`${url}/?who=bill`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ who: 'mike' }),
|
||||
headers: { 'content-type': 'application/json', cookie: 'who=jim' },
|
||||
});
|
||||
|
||||
// here we test that bodySpy is called twice with exactly the same arguments
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
expect(spy.mock.calls[0][i]).toBe(spy.mock.calls[1][i]);
|
||||
}
|
||||
});
|
||||
|
||||
test('should be able to overwrite request properties', async () => {
|
||||
const spy = jest.fn(() => {});
|
||||
|
||||
mockListener.mockImplementation((...args) => {
|
||||
nowHelpers.forEach(([prop, n]) => {
|
||||
/* eslint-disable */
|
||||
args[n][prop] = 'ok';
|
||||
args[n][prop] = 'ok2';
|
||||
spy(args[n][prop]);
|
||||
});
|
||||
|
||||
args[1].end();
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
|
||||
});
|
||||
|
||||
test('should be able to reconfig request properties', async () => {
|
||||
const spy = jest.fn(() => {});
|
||||
|
||||
mockListener.mockImplementation((...args) => {
|
||||
nowHelpers.forEach(([prop, n]) => {
|
||||
// eslint-disable-next-line
|
||||
Object.defineProperty(args[n], prop, { value: 'ok' });
|
||||
Object.defineProperty(args[n], prop, { value: 'ok2' });
|
||||
spy(args[n][prop]);
|
||||
});
|
||||
|
||||
args[1].end();
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('req.query', () => {
|
||||
test('req.query should reflect querystring in the url', async () => {
|
||||
await fetchWithProxyReq(`${url}/?who=bill&where=us`);
|
||||
|
||||
expect(mockListener.mock.calls[0][0].query).toMatchObject({
|
||||
who: 'bill',
|
||||
where: 'us',
|
||||
});
|
||||
});
|
||||
|
||||
test('req.query should turn multiple params with same name into an array', async () => {
|
||||
await fetchWithProxyReq(`${url}/?a=2&a=1`);
|
||||
|
||||
expect(mockListener.mock.calls[0][0].query).toMatchObject({
|
||||
a: ['2', '1'],
|
||||
});
|
||||
});
|
||||
|
||||
test('req.query should be {} when there is no querystring', async () => {
|
||||
await fetchWithProxyReq(url);
|
||||
const [{ query }] = mockListener.mock.calls[0];
|
||||
expect(Object.keys(query).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('req.cookies', () => {
|
||||
test('req.cookies should reflect req.cookie header', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
headers: {
|
||||
cookie: 'who=bill; where=us',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
|
||||
who: 'bill',
|
||||
where: 'us',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('req.body', () => {
|
||||
test('req.body should be undefined by default', async () => {
|
||||
await fetchWithProxyReq(url);
|
||||
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
|
||||
});
|
||||
|
||||
test('req.body should be undefined if content-type is not defined', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello',
|
||||
});
|
||||
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
|
||||
});
|
||||
|
||||
test('req.body should be a string when content-type is `text/plain`', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello',
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toBe('hello');
|
||||
});
|
||||
|
||||
test('req.body should be a buffer when content-type is `application/octet-stream`', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello',
|
||||
headers: { 'content-type': 'application/octet-stream' },
|
||||
});
|
||||
|
||||
const [{ body }] = mockListener.mock.calls[0];
|
||||
|
||||
const str = body.toString();
|
||||
|
||||
expect(Buffer.isBuffer(body)).toBe(true);
|
||||
expect(str).toBe('hello');
|
||||
});
|
||||
|
||||
test('req.body should be an object when content-type is `application/x-www-form-urlencoded`', async () => {
|
||||
const obj = { who: 'mike' };
|
||||
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: qs.encode(obj),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toMatchObject(obj);
|
||||
});
|
||||
|
||||
test('req.body should be an object when content-type is `application/json`', async () => {
|
||||
const json = {
|
||||
who: 'bill',
|
||||
where: 'us',
|
||||
};
|
||||
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(json),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toMatchObject(json);
|
||||
});
|
||||
|
||||
test('should work when body is empty and content-type is `application/json`', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
console.log(req.body);
|
||||
res.end();
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: '',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({});
|
||||
});
|
||||
|
||||
test('should be able to try/catch parse errors', async () => {
|
||||
const bodySpy = jest.fn(() => {});
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
try {
|
||||
if (req.body === undefined) res.status(400);
|
||||
} catch (error) {
|
||||
bodySpy(error);
|
||||
} finally {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: '{"wrong":"json"',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(bodySpy).toHaveBeenCalled();
|
||||
|
||||
const [error] = bodySpy.mock.calls[0];
|
||||
expect(error.message).toMatch(/invalid json/i);
|
||||
expect(error.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('res.status', () => {
|
||||
test('res.status() should set the status code', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.status(404);
|
||||
res.end();
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test('res.status() should be chainable', async () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
spy(res, res.status(404));
|
||||
res.end();
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
const [a, b] = spy.mock.calls[0];
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('res.redirect', () => {
|
||||
test('should redirect to login', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.redirect('/login');
|
||||
res.end();
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url, { redirect: 'manual' });
|
||||
|
||||
expect(res.status).toBe(307);
|
||||
expect(res.headers.get('location')).toBe(url + '/login');
|
||||
});
|
||||
test('should redirect with status code 301', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.redirect(301, '/login');
|
||||
res.end();
|
||||
});
|
||||
const res = await fetchWithProxyReq(url, { redirect: 'manual' });
|
||||
expect(res.status).toBe(301);
|
||||
expect(res.headers.get('location')).toBe(url + '/login');
|
||||
});
|
||||
test('should show friendly error for invalid redirect', async () => {
|
||||
let error;
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
try {
|
||||
res.redirect(307);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
await fetchWithProxyReq(url, { redirect: 'manual' });
|
||||
expect(error.message).toBe(
|
||||
`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').`
|
||||
);
|
||||
});
|
||||
test('should show friendly error in case of passing null as first argument redirect', async () => {
|
||||
let error;
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
try {
|
||||
res.redirect(null);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
await fetchWithProxyReq(url, { redirect: 'manual' });
|
||||
expect(error.message).toBe(
|
||||
`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').`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// tests based on expressjs test suite
|
||||
// see https://github.com/expressjs/express/blob/master/test/res.send.js
|
||||
describe('res.send', () => {
|
||||
test('should be chainable', async () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
spy(res, res.send('hello'));
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
const [a, b] = spy.mock.calls[0];
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
describe('res.send()', () => {
|
||||
test('should set body to ""', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send();
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.send(null)', () => {
|
||||
test('should set body to ""', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(null);
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-length')).toBe('0');
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.send(undefined)', () => {
|
||||
test('should set body to ""', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(undefined);
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.send(String)', () => {
|
||||
test('should send as html', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('<p>hey</p>');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||
expect(await res.text()).toBe('<p>hey</p>');
|
||||
});
|
||||
|
||||
test('should set Content-Length', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('½ + ¼ = ¾');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(Number(res.headers.get('content-length'))).toBe(12);
|
||||
expect(await res.text()).toBe('½ + ¼ = ¾');
|
||||
});
|
||||
|
||||
test('should set ETag', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(Array(1000).join('-'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe(
|
||||
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
|
||||
);
|
||||
});
|
||||
|
||||
test('should not override Content-Type', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.send('hey');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
|
||||
expect(await res.text()).toBe('hey');
|
||||
});
|
||||
|
||||
test('should override charset in Content-Type', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=iso-8859-1');
|
||||
res.send('hey');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
|
||||
expect(await res.text()).toBe('hey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.send(Buffer)', () => {
|
||||
test('should keep charset in Content-Type', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=iso-8859-1');
|
||||
res.send(Buffer.from('hi'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe(
|
||||
'text/plain; charset=iso-8859-1'
|
||||
);
|
||||
expect(await res.text()).toBe('hi');
|
||||
});
|
||||
|
||||
test('should set Content-Length', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(Buffer.from('½ + ¼ = ¾'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(Number(res.headers.get('content-length'))).toBe(12);
|
||||
expect(await res.text()).toBe('½ + ¼ = ¾');
|
||||
});
|
||||
|
||||
test('should send as octet-stream', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(Buffer.from('hello'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe('application/octet-stream');
|
||||
expect((await res.buffer()).toString('hex')).toBe(
|
||||
Buffer.from('hello').toString('hex')
|
||||
);
|
||||
});
|
||||
|
||||
test('should set ETag', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(Buffer.alloc(999, '-'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe(
|
||||
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
|
||||
);
|
||||
});
|
||||
|
||||
test('should not override Content-Type', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(Buffer.from('hey'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
|
||||
expect(await res.text()).toBe('hey');
|
||||
});
|
||||
|
||||
test('should not override ETag', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('ETag', '"foo"');
|
||||
res.send(Buffer.from('hey'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe('"foo"');
|
||||
expect(await res.text()).toBe('hey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.send(Object)', () => {
|
||||
test('should send as application/json', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send({ name: 'tobi' });
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('{"name":"tobi"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the request method is HEAD', () => {
|
||||
test('should ignore the body', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('yay');
|
||||
});
|
||||
|
||||
// TODO: fix this test
|
||||
// node-fetch is automatically ignoring the body so this test will never fail
|
||||
const res = await fetchWithProxyReq(url, { method: 'HEAD' });
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.buffer()).toString()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when .statusCode is 204', () => {
|
||||
test('should strip Content-* fields, Transfer-Encoding field, and body', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.statusCode = 204;
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
res.send('foo');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(204);
|
||||
expect(res.headers.get('Content-Type')).toBe(null);
|
||||
expect(res.headers.get('Content-Length')).toBe(null);
|
||||
expect(res.headers.get('Transfer-Encoding')).toBe(null);
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when .statusCode is 304', () => {
|
||||
test('should strip Content-* fields, Transfer-Encoding field, and body', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.statusCode = 304;
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
res.send('foo');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(304);
|
||||
expect(res.headers.get('Content-Type')).toBe(null);
|
||||
expect(res.headers.get('Content-Length')).toBe(null);
|
||||
expect(res.headers.get('Transfer-Encoding')).toBe(null);
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// test('should always check regardless of length', async () => {
|
||||
// const etag = '"asdf"';
|
||||
|
||||
// mockListener.mockImplementation((req, res) => {
|
||||
// res.setHeader('ETag', etag);
|
||||
// res.send('hey');
|
||||
// });
|
||||
|
||||
// const res = await fetchWithProxyReq(url, {
|
||||
// headers: { 'If-None-Match': etag },
|
||||
// });
|
||||
// expect(res.status).toBe(304);
|
||||
// });
|
||||
|
||||
// test('should respond with 304 Not Modified when fresh', async () => {
|
||||
// const etag = '"asdf"';
|
||||
|
||||
// mockListener.mockImplementation((req, res) => {
|
||||
// res.setHeader('ETag', etag);
|
||||
// res.send(Array(1000).join('-'));
|
||||
// });
|
||||
|
||||
// const res = await fetchWithProxyReq(url, {
|
||||
// headers: { 'If-None-Match': etag },
|
||||
// });
|
||||
// expect(res.status).toBe(304);
|
||||
// });
|
||||
|
||||
// test('should not perform freshness check unless 2xx or 304', async () => {
|
||||
// const etag = '"asdf"';
|
||||
|
||||
// mockListener.mockImplementation((req, res) => {
|
||||
// res.status(500);
|
||||
// res.setHeader('ETag', etag);
|
||||
// res.send('hey');
|
||||
// });
|
||||
|
||||
// const res = await fetchWithProxyReq(url, {
|
||||
// headers: { 'If-None-Match': etag },
|
||||
// });
|
||||
// expect(res.status).toBe(500);
|
||||
// expect(await res.text()).toBe('hey');
|
||||
// });
|
||||
|
||||
describe('etag', () => {
|
||||
test('should send ETag', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('kajdslfkasdf');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe('W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"');
|
||||
});
|
||||
|
||||
test('should send ETag for empty string response', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send('');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe('W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"');
|
||||
});
|
||||
|
||||
test('should send ETag for long response', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send(Array(1000).join('-'));
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe(
|
||||
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
|
||||
);
|
||||
});
|
||||
|
||||
test('should not override ETag when manually set', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('etag', '"asdf"');
|
||||
res.send('hello');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe('"asdf"');
|
||||
});
|
||||
|
||||
test('should not send ETag for res.send()', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.send();
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('ETag')).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// tests based on expressjs test suite
|
||||
// see https://github.com/expressjs/express/blob/master/test/res.json.js
|
||||
describe('res.json', () => {
|
||||
test('should send be chainable', async () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
spy(res, res.json({ hello: 'world' }));
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
const [a, b] = spy.mock.calls[0];
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
test('res.json() should send an empty body', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json();
|
||||
});
|
||||
|
||||
await fetchWithProxyReq(url);
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('');
|
||||
});
|
||||
|
||||
describe('.json(object)', () => {
|
||||
test('should not override previous Content-Types', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.setHeader('content-type', 'application/vnd.example+json');
|
||||
res.json({ hello: 'world' });
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/vnd.example+json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('{"hello":"world"}');
|
||||
});
|
||||
|
||||
test('should set Content-Length and Content-Type', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json({ hello: '½ + ¼ = ¾' });
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(Number(res.headers.get('content-length'))).toBe(24);
|
||||
expect(await res.text()).toBe('{"hello":"½ + ¼ = ¾"}');
|
||||
});
|
||||
|
||||
describe('when given primitives', () => {
|
||||
test('should respond with json for null', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json(null);
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('null');
|
||||
});
|
||||
|
||||
test('should respond with json for Number', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json(300);
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('300');
|
||||
});
|
||||
|
||||
test('should respond with json for String', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json('str');
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('"str"');
|
||||
});
|
||||
});
|
||||
|
||||
test('should respond with json when given an array', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json(['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('["foo","bar","baz"]');
|
||||
});
|
||||
|
||||
test('should respond with json when given an object', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
res.json({ name: 'tobi' });
|
||||
});
|
||||
|
||||
const res = await fetchWithProxyReq(url);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe(
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(await res.text()).toBe('{"name":"tobi"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,11 @@
|
||||
/// <reference types="node" />
|
||||
import { Server, IncomingHttpHeaders, OutgoingHttpHeaders } from 'http';
|
||||
import {
|
||||
Server,
|
||||
IncomingHttpHeaders,
|
||||
OutgoingHttpHeaders,
|
||||
ServerResponse,
|
||||
IncomingMessage,
|
||||
} from 'http';
|
||||
export interface VercelProxyEvent {
|
||||
Action: string;
|
||||
body: string;
|
||||
@@ -37,3 +43,20 @@ export type LauncherConfiguration = {
|
||||
awsLambdaHandler?: string;
|
||||
useRequire?: boolean;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node",
|
||||
"version": "1.13.0",
|
||||
"version": "1.13.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -32,10 +32,10 @@
|
||||
"@types/cookie": "0.3.3",
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.5",
|
||||
"@vercel/node-bridge": "2.1.1",
|
||||
"@vercel/node-bridge": "2.1.2-canary.0",
|
||||
"content-type": "1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"etag": "1.8.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/python",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1-canary.0",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/execa": "^0.9.0",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"execa": "^1.0.0",
|
||||
"typescript": "4.3.4"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@vercel/ruby",
|
||||
"author": "Nathan Cahill <nathan@nathancahill.com>",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "8.0.0",
|
||||
"@types/semver": "6.0.0",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/build-utils": "2.14.1-canary.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"execa": "2.0.4",
|
||||
"fs-extra": "^7.0.1",
|
||||
|
||||
Reference in New Issue
Block a user