mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-12 04:22:14 +00:00
Compare commits
9 Commits
@vercel/py
...
@vercel/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99fa729966 | ||
|
|
2bb3da80e0 | ||
|
|
b852f34a27 | ||
|
|
ce8e6e3806 | ||
|
|
983946650e | ||
|
|
59e4572e76 | ||
|
|
5c297122cb | ||
|
|
28f3bf9ef6 | ||
|
|
a936e92b8b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,5 +27,4 @@ test/lib/deployment/failed-page.txt
|
|||||||
/public
|
/public
|
||||||
__pycache__
|
__pycache__
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
.turbo
|
.turbo
|
||||||
|
|||||||
1
examples/angular/.gitignore
vendored
1
examples/angular/.gitignore
vendored
@@ -41,4 +41,3 @@ testem.log
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
|
|||||||
1
examples/remix/.gitignore
vendored
1
examples/remix/.gitignore
vendored
@@ -2,7 +2,6 @@ node_modules
|
|||||||
|
|
||||||
.cache
|
.cache
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
|
|
||||||
public/build
|
public/build
|
||||||
api/_build
|
api/_build
|
||||||
|
|||||||
1
examples/solidstart/.gitignore
vendored
1
examples/solidstart/.gitignore
vendored
@@ -2,7 +2,6 @@ dist
|
|||||||
worker
|
worker
|
||||||
.solid
|
.solid
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|||||||
1
examples/sveltekit/.gitignore
vendored
1
examples/sveltekit/.gitignore
vendored
@@ -7,4 +7,3 @@ node_modules
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/build-utils",
|
"name": "@vercel/build-utils",
|
||||||
"version": "2.13.1-canary.1",
|
"version": "2.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.js",
|
"types": "./dist/index.d.js",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@types/node-fetch": "^2.1.6",
|
"@types/node-fetch": "^2.1.6",
|
||||||
"@types/semver": "6.0.0",
|
"@types/semver": "6.0.0",
|
||||||
"@types/yazl": "^2.4.1",
|
"@types/yazl": "^2.4.1",
|
||||||
"@vercel/frameworks": "0.5.1-canary.21",
|
"@vercel/frameworks": "0.6.0",
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"aggregate-error": "3.0.1",
|
"aggregate-error": "3.0.1",
|
||||||
"async-retry": "1.2.3",
|
"async-retry": "1.2.3",
|
||||||
|
|||||||
@@ -1,411 +0,0 @@
|
|||||||
import fs from 'fs-extra';
|
|
||||||
import { join, parse, relative, dirname, basename, extname } from 'path';
|
|
||||||
import glob from './fs/glob';
|
|
||||||
import { normalizePath } from './fs/normalize-path';
|
|
||||||
import { Lambda } from './lambda';
|
|
||||||
import type { BuildOptions } from './types';
|
|
||||||
import { debug, getIgnoreFilter } from '.';
|
|
||||||
|
|
||||||
// `.output` was already created by the Build Command, so we have
|
|
||||||
// to ensure its contents don't get bundled into the Lambda. Similarily,
|
|
||||||
// we don't want to bundle anything from `.vercel` either. Lastly,
|
|
||||||
// Builders/Runtimes didn't have `vercel.json` or `now.json`.
|
|
||||||
const ignoredPaths = ['.output', '.vercel', 'vercel.json', 'now.json'];
|
|
||||||
|
|
||||||
const shouldIgnorePath = (
|
|
||||||
file: string,
|
|
||||||
ignoreFilter: any,
|
|
||||||
ignoreFile: boolean
|
|
||||||
) => {
|
|
||||||
const isNative = ignoredPaths.some(item => {
|
|
||||||
return file.startsWith(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ignoreFile) {
|
|
||||||
return isNative;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isNative || ignoreFilter(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSourceFiles = async (workPath: string, ignoreFilter: any) => {
|
|
||||||
const list = await glob('**', {
|
|
||||||
cwd: workPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
// We're not passing this as an `ignore` filter to the `glob` function above,
|
|
||||||
// so that we can re-use exactly the same `getIgnoreFilter` method that the
|
|
||||||
// Build Step uses (literally the same code). Note that this exclusion only applies
|
|
||||||
// when deploying. Locally, another exclusion is needed, which is handled
|
|
||||||
// further below in the `convertRuntimeToPlugin` function.
|
|
||||||
for (const file in list) {
|
|
||||||
if (shouldIgnorePath(file, ignoreFilter, true)) {
|
|
||||||
delete list[file];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert legacy Runtime to a Plugin.
|
|
||||||
* @param buildRuntime - a legacy build() function from a Runtime
|
|
||||||
* @param packageName - the name of the package, for example `vercel-plugin-python`
|
|
||||||
* @param ext - the file extension, for example `.py`
|
|
||||||
*/
|
|
||||||
export function _experimental_convertRuntimeToPlugin(
|
|
||||||
buildRuntime: (options: BuildOptions) => Promise<{ output: Lambda }>,
|
|
||||||
packageName: string,
|
|
||||||
ext: string
|
|
||||||
) {
|
|
||||||
// This `build()` signature should match `plugin.build()` signature in `vercel build`.
|
|
||||||
return async function build({ workPath }: { workPath: string }) {
|
|
||||||
// We also don't want to provide any files to Runtimes that were ignored
|
|
||||||
// through `.vercelignore` or `.nowignore`, because the Build Step does the same.
|
|
||||||
const ignoreFilter = await getIgnoreFilter(workPath);
|
|
||||||
|
|
||||||
// Retrieve the files that are currently available on the File System,
|
|
||||||
// before the Legacy Runtime has even started to build.
|
|
||||||
const sourceFilesPreBuild = await getSourceFiles(workPath, ignoreFilter);
|
|
||||||
|
|
||||||
// Instead of doing another `glob` to get all the matching source files,
|
|
||||||
// we'll filter the list of existing files down to only the ones
|
|
||||||
// that are matching the entrypoint pattern, so we're first creating
|
|
||||||
// a clean new list to begin.
|
|
||||||
const entrypoints = Object.assign({}, sourceFilesPreBuild);
|
|
||||||
|
|
||||||
const entrypointMatch = new RegExp(`^api/.*${ext}$`);
|
|
||||||
|
|
||||||
// Up next, we'll strip out the files from the list of entrypoints
|
|
||||||
// that aren't actually considered entrypoints.
|
|
||||||
for (const file in entrypoints) {
|
|
||||||
if (!entrypointMatch.test(file)) {
|
|
||||||
delete entrypoints[file];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages: { [key: string]: any } = {};
|
|
||||||
const pluginName = packageName.replace('vercel-plugin-', '');
|
|
||||||
const outputPath = join(workPath, '.output');
|
|
||||||
|
|
||||||
const traceDir = join(
|
|
||||||
outputPath,
|
|
||||||
`inputs`,
|
|
||||||
// Legacy Runtimes can only provide API Routes, so that's
|
|
||||||
// why we can use this prefix for all of them. Here, we have to
|
|
||||||
// make sure to not use a cryptic hash name, because people
|
|
||||||
// need to be able to easily inspect the output.
|
|
||||||
`api-routes-${pluginName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.ensureDir(traceDir);
|
|
||||||
|
|
||||||
const entryRoot = join(outputPath, 'server', 'pages');
|
|
||||||
|
|
||||||
for (const entrypoint of Object.keys(entrypoints)) {
|
|
||||||
const { output } = await buildRuntime({
|
|
||||||
files: sourceFilesPreBuild,
|
|
||||||
entrypoint,
|
|
||||||
workPath,
|
|
||||||
config: {
|
|
||||||
zeroConfig: true,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
avoidTopLevelInstall: true,
|
|
||||||
skipDownload: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const lambdaFiles = output.files;
|
|
||||||
|
|
||||||
// When deploying, the `files` that are passed to the Legacy Runtimes already
|
|
||||||
// have certain files that are ignored stripped, but locally, that list of
|
|
||||||
// files isn't used by the Legacy Runtimes, so we need to apply the filters
|
|
||||||
// to the outputs that they are returning instead.
|
|
||||||
for (const file in lambdaFiles) {
|
|
||||||
if (shouldIgnorePath(file, ignoreFilter, false)) {
|
|
||||||
delete lambdaFiles[file];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handlerFileBase = output.handler;
|
|
||||||
let handlerFile = lambdaFiles[handlerFileBase];
|
|
||||||
let handlerHasImport = false;
|
|
||||||
|
|
||||||
const { handler } = output;
|
|
||||||
const handlerMethod = handler.split('.').pop();
|
|
||||||
const handlerFileName = handler.replace(`.${handlerMethod}`, '');
|
|
||||||
|
|
||||||
// For compiled languages, the launcher file for the Lambda generated
|
|
||||||
// by the Legacy Runtime matches the `handler` defined for it, but for
|
|
||||||
// interpreted languages, the `handler` consists of the launcher file name
|
|
||||||
// without an extension, plus the name of the method inside of that file
|
|
||||||
// that should be invoked, so we have to construct the file path explicitly.
|
|
||||||
if (!handlerFile) {
|
|
||||||
handlerFileBase = handlerFileName + ext;
|
|
||||||
handlerFile = lambdaFiles[handlerFileBase];
|
|
||||||
handlerHasImport = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handlerFile || !handlerFile.fsPath) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find a handler file. Please ensure that \`files\` for the returned \`Lambda\` contains an \`FileFsRef\` named "${handlerFileBase}" with a valid \`fsPath\`.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlerExtName = extname(handlerFile.fsPath);
|
|
||||||
|
|
||||||
const entryBase = basename(entrypoint).replace(ext, handlerExtName);
|
|
||||||
const entryPath = join(dirname(entrypoint), entryBase);
|
|
||||||
const entry = join(entryRoot, entryPath);
|
|
||||||
|
|
||||||
// Create the parent directory of the API Route that will be created
|
|
||||||
// for the current entrypoint inside of `.output/server/pages/api`.
|
|
||||||
await fs.ensureDir(dirname(entry));
|
|
||||||
|
|
||||||
// For compiled languages, the launcher file will be binary and therefore
|
|
||||||
// won't try to import a user-provided request handler (instead, it will
|
|
||||||
// contain it). But for interpreted languages, the launcher might try to
|
|
||||||
// load a user-provided request handler from the source file instead of bundling
|
|
||||||
// it, so we have to adjust the import statement inside the launcher to point
|
|
||||||
// to the respective source file. Previously, Legacy Runtimes simply expected
|
|
||||||
// the user-provided request-handler to be copied right next to the launcher,
|
|
||||||
// but with the new File System API, files won't be moved around unnecessarily.
|
|
||||||
if (handlerHasImport) {
|
|
||||||
const { fsPath } = handlerFile;
|
|
||||||
const encoding = 'utf-8';
|
|
||||||
|
|
||||||
// This is the true directory of the user-provided request handler in the
|
|
||||||
// source files, so that's what we will use as an import path in the launcher.
|
|
||||||
const locationPrefix = relative(entry, outputPath);
|
|
||||||
|
|
||||||
let handlerContent = await fs.readFile(fsPath, encoding);
|
|
||||||
|
|
||||||
const importPaths = [
|
|
||||||
// This is the full entrypoint path, like `./api/test.py`. In our tests
|
|
||||||
// Python didn't support importing from a parent directory without using different
|
|
||||||
// code in the launcher that registers it as a location for modules and then changing
|
|
||||||
// the importing syntax, but continuing to import it like before seems to work. If
|
|
||||||
// other languages need this, we should consider excluding Python explicitly.
|
|
||||||
// `./${entrypoint}`,
|
|
||||||
|
|
||||||
// This is the entrypoint path without extension, like `api/test`
|
|
||||||
entrypoint.slice(0, -ext.length),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate a list of regular expressions that we can use for
|
|
||||||
// finding matches, but only allow matches if the import path is
|
|
||||||
// wrapped inside single (') or double quotes (").
|
|
||||||
const patterns = importPaths.map(path => {
|
|
||||||
// eslint-disable-next-line no-useless-escape
|
|
||||||
return new RegExp(`('|")(${path.replace(/\./g, '\\.')})('|")`, 'g');
|
|
||||||
});
|
|
||||||
|
|
||||||
let replacedMatch = null;
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const newContent = handlerContent.replace(
|
|
||||||
pattern,
|
|
||||||
(_, p1, p2, p3) => {
|
|
||||||
return `${p1}${join(locationPrefix, p2)}${p3}`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newContent !== handlerContent) {
|
|
||||||
debug(
|
|
||||||
`Replaced "${pattern}" inside "${entry}" to ensure correct import of user-provided request handler`
|
|
||||||
);
|
|
||||||
|
|
||||||
handlerContent = newContent;
|
|
||||||
replacedMatch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!replacedMatch) {
|
|
||||||
new Error(
|
|
||||||
`No replacable matches for "${importPaths[0]}" or "${importPaths[1]}" found in "${fsPath}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(entry, handlerContent, encoding);
|
|
||||||
} else {
|
|
||||||
await fs.copy(handlerFile.fsPath, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy Runtimes based on interpreted languages will create a new launcher file
|
|
||||||
// for every entrypoint, but they will create each one inside `workPath`, which means that
|
|
||||||
// the launcher for one entrypoint will overwrite the launcher provided for the previous
|
|
||||||
// entrypoint. That's why, above, we copy the file contents into the new destination (and
|
|
||||||
// optionally transform them along the way), instead of linking. We then also want to remove
|
|
||||||
// the copy origin right here, so that the `workPath` doesn't contain a useless launcher file
|
|
||||||
// once the build has finished running.
|
|
||||||
await fs.remove(handlerFile.fsPath);
|
|
||||||
debug(`Removed temporary file "${handlerFile.fsPath}"`);
|
|
||||||
|
|
||||||
const nft = `${entry}.nft.json`;
|
|
||||||
|
|
||||||
const json = JSON.stringify({
|
|
||||||
version: 2,
|
|
||||||
files: Object.keys(lambdaFiles)
|
|
||||||
.map(file => {
|
|
||||||
const { fsPath } = lambdaFiles[file];
|
|
||||||
|
|
||||||
if (!fsPath) {
|
|
||||||
throw new Error(
|
|
||||||
`File "${file}" is missing valid \`fsPath\` property`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The handler was already moved into position above.
|
|
||||||
if (file === handlerFileBase) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizePath(relative(dirname(nft), fsPath));
|
|
||||||
})
|
|
||||||
.filter(Boolean),
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.writeFile(nft, json);
|
|
||||||
|
|
||||||
// Add an entry that will later on be added to the `functions-manifest.json`
|
|
||||||
// file that is placed inside of the `.output` directory.
|
|
||||||
pages[normalizePath(entryPath)] = {
|
|
||||||
// Because the underlying file used as a handler was placed
|
|
||||||
// inside `.output/server/pages/api`, it no longer has the name it originally
|
|
||||||
// had and is now named after the API Route that it's responsible for,
|
|
||||||
// so we have to adjust the name of the Lambda handler accordingly.
|
|
||||||
handler: handler.replace(handlerFileName, parse(entry).name),
|
|
||||||
runtime: output.runtime,
|
|
||||||
memory: output.memory,
|
|
||||||
maxDuration: output.maxDuration,
|
|
||||||
environment: output.environment,
|
|
||||||
allowQuery: output.allowQuery,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any Serverless Functions that were exposed by the Legacy Runtime
|
|
||||||
// to the `functions-manifest.json` file provided in `.output`.
|
|
||||||
await _experimental_updateFunctionsManifest({ workPath, pages });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If `.output/functions-manifest.json` exists, append to the pages
|
|
||||||
* property. Otherwise write a new file.
|
|
||||||
*/
|
|
||||||
export async function _experimental_updateFunctionsManifest({
|
|
||||||
workPath,
|
|
||||||
pages,
|
|
||||||
}: {
|
|
||||||
workPath: string;
|
|
||||||
pages: { [key: string]: any };
|
|
||||||
}) {
|
|
||||||
const functionsManifestPath = join(
|
|
||||||
workPath,
|
|
||||||
'.output',
|
|
||||||
'functions-manifest.json'
|
|
||||||
);
|
|
||||||
const functionsManifest = await readJson(functionsManifestPath);
|
|
||||||
|
|
||||||
if (!functionsManifest.version) functionsManifest.version = 2;
|
|
||||||
if (!functionsManifest.pages) functionsManifest.pages = {};
|
|
||||||
|
|
||||||
for (const [pageKey, pageConfig] of Object.entries(pages)) {
|
|
||||||
functionsManifest.pages[pageKey] = { ...pageConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(functionsManifestPath, JSON.stringify(functionsManifest));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append routes to the `routes-manifest.json` file.
|
|
||||||
* If the file does not exist, it will be created.
|
|
||||||
*/
|
|
||||||
export async function _experimental_updateRoutesManifest({
|
|
||||||
workPath,
|
|
||||||
redirects,
|
|
||||||
rewrites,
|
|
||||||
headers,
|
|
||||||
dynamicRoutes,
|
|
||||||
staticRoutes,
|
|
||||||
}: {
|
|
||||||
workPath: string;
|
|
||||||
redirects?: {
|
|
||||||
source: string;
|
|
||||||
destination: string;
|
|
||||||
statusCode: number;
|
|
||||||
regex: string;
|
|
||||||
}[];
|
|
||||||
rewrites?: {
|
|
||||||
source: string;
|
|
||||||
destination: string;
|
|
||||||
regex: string;
|
|
||||||
}[];
|
|
||||||
headers?: {
|
|
||||||
source: string;
|
|
||||||
headers: {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
regex: string;
|
|
||||||
}[];
|
|
||||||
dynamicRoutes?: {
|
|
||||||
page: string;
|
|
||||||
regex: string;
|
|
||||||
namedRegex?: string;
|
|
||||||
routeKeys?: { [named: string]: string };
|
|
||||||
}[];
|
|
||||||
staticRoutes?: {
|
|
||||||
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 = 3;
|
|
||||||
if (routesManifest.pages404 === undefined) routesManifest.pages404 = true;
|
|
||||||
|
|
||||||
if (redirects) {
|
|
||||||
if (!routesManifest.redirects) routesManifest.redirects = [];
|
|
||||||
routesManifest.redirects.push(...redirects);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rewrites) {
|
|
||||||
if (!routesManifest.rewrites) routesManifest.rewrites = [];
|
|
||||||
routesManifest.rewrites.push(...rewrites);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (headers) {
|
|
||||||
if (!routesManifest.headers) routesManifest.headers = [];
|
|
||||||
routesManifest.headers.push(...headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dynamicRoutes) {
|
|
||||||
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
|
|
||||||
routesManifest.dynamicRoutes.push(...dynamicRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (staticRoutes) {
|
|
||||||
if (!routesManifest.staticRoutes) routesManifest.staticRoutes = [];
|
|
||||||
routesManifest.staticRoutes.push(...staticRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getPlatformEnv } from './';
|
import { getPlatformEnv } from './get-platform-env';
|
||||||
|
|
||||||
export default function debug(message: string, ...additional: any[]) {
|
export default function debug(message: string, ...additional: any[]) {
|
||||||
if (getPlatformEnv('BUILDER_DEBUG')) {
|
if (getPlatformEnv('BUILDER_DEBUG')) {
|
||||||
|
|||||||
23
packages/build-utils/src/get-platform-env.ts
Normal file
23
packages/build-utils/src/get-platform-env.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { NowBuildError } from './errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to support both `VERCEL_` and legacy `NOW_` env vars.
|
||||||
|
* Throws an error if *both* env vars are defined.
|
||||||
|
*/
|
||||||
|
export const getPlatformEnv = (name: string): string | undefined => {
|
||||||
|
const vName = `VERCEL_${name}`;
|
||||||
|
const nName = `NOW_${name}`;
|
||||||
|
const v = process.env[vName];
|
||||||
|
const n = process.env[nName];
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
if (typeof n === 'string') {
|
||||||
|
throw new NowBuildError({
|
||||||
|
code: 'CONFLICTING_ENV_VAR_NAMES',
|
||||||
|
message: `Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var.`,
|
||||||
|
link: 'https://vercel.link/combining-old-and-new-config',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import FileBlob from './file-blob';
|
|||||||
import FileFsRef from './file-fs-ref';
|
import FileFsRef from './file-fs-ref';
|
||||||
import FileRef from './file-ref';
|
import FileRef from './file-ref';
|
||||||
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
|
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
|
||||||
|
import { NodejsLambda } from './nodejs-lambda';
|
||||||
import { Prerender } from './prerender';
|
import { Prerender } from './prerender';
|
||||||
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
|
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
|
||||||
import getWriteableDirectory from './fs/get-writable-directory';
|
import getWriteableDirectory from './fs/get-writable-directory';
|
||||||
@@ -31,17 +32,18 @@ import {
|
|||||||
getLatestNodeVersion,
|
getLatestNodeVersion,
|
||||||
getDiscontinuedNodeVersions,
|
getDiscontinuedNodeVersions,
|
||||||
} from './fs/node-version';
|
} from './fs/node-version';
|
||||||
import { NowBuildError } from './errors';
|
|
||||||
import streamToBuffer from './fs/stream-to-buffer';
|
import streamToBuffer from './fs/stream-to-buffer';
|
||||||
import shouldServe from './should-serve';
|
import shouldServe from './should-serve';
|
||||||
import debug from './debug';
|
import debug from './debug';
|
||||||
import getIgnoreFilter from './get-ignore-filter';
|
import getIgnoreFilter from './get-ignore-filter';
|
||||||
|
import { getPlatformEnv } from './get-platform-env';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FileBlob,
|
FileBlob,
|
||||||
FileFsRef,
|
FileFsRef,
|
||||||
FileRef,
|
FileRef,
|
||||||
Lambda,
|
Lambda,
|
||||||
|
NodejsLambda,
|
||||||
createLambda,
|
createLambda,
|
||||||
Prerender,
|
Prerender,
|
||||||
download,
|
download,
|
||||||
@@ -69,6 +71,7 @@ export {
|
|||||||
getLatestNodeVersion,
|
getLatestNodeVersion,
|
||||||
getDiscontinuedNodeVersions,
|
getDiscontinuedNodeVersions,
|
||||||
getSpawnOptions,
|
getSpawnOptions,
|
||||||
|
getPlatformEnv,
|
||||||
streamToBuffer,
|
streamToBuffer,
|
||||||
shouldServe,
|
shouldServe,
|
||||||
debug,
|
debug,
|
||||||
@@ -89,11 +92,6 @@ export { detectFramework } from './detect-framework';
|
|||||||
export { DetectorFilesystem } from './detectors/filesystem';
|
export { DetectorFilesystem } from './detectors/filesystem';
|
||||||
export { readConfigFile } from './fs/read-config-file';
|
export { readConfigFile } from './fs/read-config-file';
|
||||||
export { normalizePath } from './fs/normalize-path';
|
export { normalizePath } from './fs/normalize-path';
|
||||||
export {
|
|
||||||
_experimental_convertRuntimeToPlugin,
|
|
||||||
_experimental_updateFunctionsManifest,
|
|
||||||
_experimental_updateRoutesManifest,
|
|
||||||
} from './convert-runtime-to-plugin';
|
|
||||||
|
|
||||||
export * from './schemas';
|
export * from './schemas';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
@@ -117,25 +115,3 @@ export const isOfficialRuntime = (desired: string, name?: string): boolean => {
|
|||||||
export const isStaticRuntime = (name?: string): boolean => {
|
export const isStaticRuntime = (name?: string): boolean => {
|
||||||
return isOfficialRuntime('static', name);
|
return isOfficialRuntime('static', name);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to support both `VERCEL_` and legacy `NOW_` env vars.
|
|
||||||
* Throws an error if *both* env vars are defined.
|
|
||||||
*/
|
|
||||||
export const getPlatformEnv = (name: string): string | undefined => {
|
|
||||||
const vName = `VERCEL_${name}`;
|
|
||||||
const nName = `NOW_${name}`;
|
|
||||||
const v = process.env[vName];
|
|
||||||
const n = process.env[nName];
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
if (typeof n === 'string') {
|
|
||||||
throw new NowBuildError({
|
|
||||||
code: 'CONFLICTING_ENV_VAR_NAMES',
|
|
||||||
message: `Both "${vName}" and "${nName}" env vars are defined. Please only define the "${vName}" env var.`,
|
|
||||||
link: 'https://vercel.link/combining-old-and-new-config',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Environment {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LambdaOptions {
|
export interface LambdaOptions {
|
||||||
files: Files;
|
files: Files;
|
||||||
handler: string;
|
handler: string;
|
||||||
runtime: string;
|
runtime: string;
|
||||||
@@ -21,6 +21,10 @@ interface LambdaOptions {
|
|||||||
environment?: Environment;
|
environment?: Environment;
|
||||||
allowQuery?: string[];
|
allowQuery?: string[];
|
||||||
regions?: string[];
|
regions?: string[];
|
||||||
|
/**
|
||||||
|
* @deprecated Use `files` property instead.
|
||||||
|
*/
|
||||||
|
zipBuffer?: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetLambdaOptionsFromFunctionOptions {
|
interface GetLambdaOptionsFromFunctionOptions {
|
||||||
@@ -29,19 +33,19 @@ interface GetLambdaOptionsFromFunctionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Lambda {
|
export class Lambda {
|
||||||
public type: 'Lambda';
|
type: 'Lambda';
|
||||||
public files: Files;
|
files: Files;
|
||||||
public handler: string;
|
handler: string;
|
||||||
public runtime: string;
|
runtime: string;
|
||||||
public memory?: number;
|
memory?: number;
|
||||||
public maxDuration?: number;
|
maxDuration?: number;
|
||||||
public environment: Environment;
|
environment: Environment;
|
||||||
public allowQuery?: string[];
|
allowQuery?: string[];
|
||||||
public regions?: string[];
|
regions?: string[];
|
||||||
/**
|
/**
|
||||||
* @deprecated Use `await lambda.createZip()` instead.
|
* @deprecated Use `await lambda.createZip()` instead.
|
||||||
*/
|
*/
|
||||||
public zipBuffer?: Buffer;
|
zipBuffer?: Buffer;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
files,
|
files,
|
||||||
@@ -52,8 +56,11 @@ export class Lambda {
|
|||||||
environment = {},
|
environment = {},
|
||||||
allowQuery,
|
allowQuery,
|
||||||
regions,
|
regions,
|
||||||
|
zipBuffer,
|
||||||
}: LambdaOptions) {
|
}: LambdaOptions) {
|
||||||
assert(typeof files === 'object', '"files" must be an object');
|
if (!zipBuffer) {
|
||||||
|
assert(typeof files === 'object', '"files" must be an object');
|
||||||
|
}
|
||||||
assert(typeof handler === 'string', '"handler" is not a string');
|
assert(typeof handler === 'string', '"handler" is not a string');
|
||||||
assert(typeof runtime === 'string', '"runtime" is not a string');
|
assert(typeof runtime === 'string', '"runtime" is not a string');
|
||||||
assert(typeof environment === 'object', '"environment" is not an object');
|
assert(typeof environment === 'object', '"environment" is not an object');
|
||||||
@@ -90,6 +97,7 @@ export class Lambda {
|
|||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
this.allowQuery = allowQuery;
|
this.allowQuery = allowQuery;
|
||||||
this.regions = regions;
|
this.regions = regions;
|
||||||
|
this.zipBuffer = zipBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createZip(): Promise<Buffer> {
|
async createZip(): Promise<Buffer> {
|
||||||
|
|||||||
27
packages/build-utils/src/nodejs-lambda.ts
Normal file
27
packages/build-utils/src/nodejs-lambda.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Lambda, LambdaOptions } from './lambda';
|
||||||
|
|
||||||
|
interface NodejsLambdaOptions extends LambdaOptions {
|
||||||
|
shouldAddHelpers: boolean;
|
||||||
|
shouldAddSourcemapSupport: boolean;
|
||||||
|
awsLambdaHandler?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodejsLambda extends Lambda {
|
||||||
|
launcherType: 'Nodejs';
|
||||||
|
shouldAddHelpers: boolean;
|
||||||
|
shouldAddSourcemapSupport: boolean;
|
||||||
|
awsLambdaHandler?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
shouldAddHelpers,
|
||||||
|
shouldAddSourcemapSupport,
|
||||||
|
awsLambdaHandler,
|
||||||
|
...opts
|
||||||
|
}: NodejsLambdaOptions) {
|
||||||
|
super(opts);
|
||||||
|
this.launcherType = 'Nodejs';
|
||||||
|
this.shouldAddHelpers = shouldAddHelpers;
|
||||||
|
this.shouldAddSourcemapSupport = shouldAddSourcemapSupport;
|
||||||
|
this.awsLambdaHandler = awsLambdaHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import FileRef from './file-ref';
|
import FileRef from './file-ref';
|
||||||
import FileFsRef from './file-fs-ref';
|
import FileFsRef from './file-fs-ref';
|
||||||
|
import { Lambda } from './lambda';
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
[name: string]: string | undefined;
|
[name: string]: string | undefined;
|
||||||
@@ -368,3 +369,49 @@ export interface ProjectSettings {
|
|||||||
directoryListing?: boolean;
|
directoryListing?: boolean;
|
||||||
gitForkProtection?: boolean;
|
gitForkProtection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BuilderV2 {
|
||||||
|
version: 2;
|
||||||
|
build: BuildV2;
|
||||||
|
prepareCache?: PrepareCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuilderV3 {
|
||||||
|
version: 3;
|
||||||
|
build: BuildV3;
|
||||||
|
prepareCache?: PrepareCache;
|
||||||
|
startDevServer?: StartDevServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageFormat = 'image/avif' | 'image/webp';
|
||||||
|
|
||||||
|
export interface Images {
|
||||||
|
domains: string[];
|
||||||
|
sizes: number[];
|
||||||
|
minimumCacheTTL?: number;
|
||||||
|
formats?: ImageFormat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildResultV2 {
|
||||||
|
// TODO: use proper `Route` type from `routing-utils` (perhaps move types to a common package)
|
||||||
|
routes: any[];
|
||||||
|
images?: Images;
|
||||||
|
output: {
|
||||||
|
[key: string]: File | Lambda;
|
||||||
|
};
|
||||||
|
wildcard?: Array<{
|
||||||
|
domain: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildResultV3 {
|
||||||
|
output: Lambda;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BuildV2 = (options: BuildOptions) => Promise<BuildResultV2>;
|
||||||
|
export type BuildV3 = (options: BuildOptions) => Promise<BuildResultV3>;
|
||||||
|
export type PrepareCache = (options: PrepareCacheOptions) => Promise<Files>;
|
||||||
|
export type StartDevServer = (
|
||||||
|
options: StartDevServerOptions
|
||||||
|
) => Promise<StartDevServerResult>;
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import { BuildOptions, createLambda, FileFsRef } from '../src';
|
|
||||||
import { _experimental_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 invalidFuncWorkpath = join(
|
|
||||||
__dirname,
|
|
||||||
'convert-runtime',
|
|
||||||
'invalid-functions'
|
|
||||||
);
|
|
||||||
const pythonApiWorkpath = join(__dirname, 'convert-runtime', 'python-api');
|
|
||||||
|
|
||||||
describe('convert-runtime-to-plugin', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
await fs.remove(join(invalidFuncWorkpath, '.output'));
|
|
||||||
await fs.remove(join(pythonApiWorkpath, '.output'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create correct fileystem for python', async () => {
|
|
||||||
const ext = '.py';
|
|
||||||
const workPath = pythonApiWorkpath;
|
|
||||||
const handlerName = 'vc__handler__python';
|
|
||||||
const handlerFileName = handlerName + ext;
|
|
||||||
|
|
||||||
const lambdaOptions = {
|
|
||||||
handler: `${handlerName}.vc_handler`,
|
|
||||||
runtime: 'python3.9',
|
|
||||||
memory: 512,
|
|
||||||
maxDuration: 5,
|
|
||||||
environment: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildRuntime = async (opts: BuildOptions) => {
|
|
||||||
const handlerPath = join(workPath, handlerFileName);
|
|
||||||
|
|
||||||
// This is the usual time at which a Legacy Runtime writes its Lambda launcher.
|
|
||||||
await fs.writeFile(handlerPath, '# handler');
|
|
||||||
|
|
||||||
opts.files[handlerFileName] = new FileFsRef({
|
|
||||||
fsPath: handlerPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lambda = await createLambda({
|
|
||||||
files: opts.files,
|
|
||||||
...lambdaOptions,
|
|
||||||
});
|
|
||||||
return { output: lambda };
|
|
||||||
};
|
|
||||||
|
|
||||||
const packageName = 'vercel-plugin-python';
|
|
||||||
const build = await _experimental_convertRuntimeToPlugin(
|
|
||||||
buildRuntime,
|
|
||||||
packageName,
|
|
||||||
ext
|
|
||||||
);
|
|
||||||
|
|
||||||
await build({ workPath });
|
|
||||||
|
|
||||||
const output = await fsToJson(join(workPath, '.output'));
|
|
||||||
|
|
||||||
expect(output).toMatchObject({
|
|
||||||
'functions-manifest.json': expect.stringContaining('{'),
|
|
||||||
server: {
|
|
||||||
pages: {
|
|
||||||
api: {
|
|
||||||
'index.py': expect.stringContaining('handler'),
|
|
||||||
'index.py.nft.json': expect.stringContaining('{'),
|
|
||||||
users: {
|
|
||||||
'get.py': expect.stringContaining('handler'),
|
|
||||||
'get.py.nft.json': expect.stringContaining('{'),
|
|
||||||
'post.py': expect.stringContaining('handler'),
|
|
||||||
'post.py.nft.json': expect.stringContaining('{'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const funcManifest = JSON.parse(output['functions-manifest.json']);
|
|
||||||
expect(funcManifest).toMatchObject({
|
|
||||||
version: 2,
|
|
||||||
pages: {
|
|
||||||
'api/index.py': { ...lambdaOptions, handler: 'index.vc_handler' },
|
|
||||||
'api/users/get.py': { ...lambdaOptions, handler: 'get.vc_handler' },
|
|
||||||
'api/users/post.py': {
|
|
||||||
...lambdaOptions,
|
|
||||||
handler: 'post.vc_handler',
|
|
||||||
memory: 512,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
|
|
||||||
expect(indexJson).toMatchObject({
|
|
||||||
version: 2,
|
|
||||||
files: [
|
|
||||||
'../../../../api/db/[id].py',
|
|
||||||
'../../../../api/index.py',
|
|
||||||
'../../../../api/project/[aid]/[bid]/index.py',
|
|
||||||
'../../../../api/users/get.py',
|
|
||||||
'../../../../api/users/post.py',
|
|
||||||
'../../../../file.txt',
|
|
||||||
'../../../../util/date.py',
|
|
||||||
'../../../../util/math.py',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getJson = JSON.parse(
|
|
||||||
output.server.pages.api.users['get.py.nft.json']
|
|
||||||
);
|
|
||||||
expect(getJson).toMatchObject({
|
|
||||||
version: 2,
|
|
||||||
files: [
|
|
||||||
'../../../../../api/db/[id].py',
|
|
||||||
'../../../../../api/index.py',
|
|
||||||
'../../../../../api/project/[aid]/[bid]/index.py',
|
|
||||||
'../../../../../api/users/get.py',
|
|
||||||
'../../../../../api/users/post.py',
|
|
||||||
'../../../../../file.txt',
|
|
||||||
'../../../../../util/date.py',
|
|
||||||
'../../../../../util/math.py',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const postJson = JSON.parse(
|
|
||||||
output.server.pages.api.users['post.py.nft.json']
|
|
||||||
);
|
|
||||||
expect(postJson).toMatchObject({
|
|
||||||
version: 2,
|
|
||||||
files: [
|
|
||||||
'../../../../../api/db/[id].py',
|
|
||||||
'../../../../../api/index.py',
|
|
||||||
'../../../../../api/project/[aid]/[bid]/index.py',
|
|
||||||
'../../../../../api/users/get.py',
|
|
||||||
'../../../../../api/users/post.py',
|
|
||||||
'../../../../../file.txt',
|
|
||||||
'../../../../../util/date.py',
|
|
||||||
'../../../../../util/math.py',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(output.server.pages['file.txt']).toBeUndefined();
|
|
||||||
expect(output.server.pages.api['file.txt']).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
21
packages/build-utils/test/unit.nodejs-lambda.test.ts
vendored
Normal file
21
packages/build-utils/test/unit.nodejs-lambda.test.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NodejsLambda, FileBlob } from '../src';
|
||||||
|
|
||||||
|
describe('Test `NodejsLambda`', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const helloSrc = 'module.exports = (req, res) => res.end("hi");';
|
||||||
|
const lambda = new NodejsLambda({
|
||||||
|
files: {
|
||||||
|
'api/hello.js': new FileBlob({ data: helloSrc }),
|
||||||
|
},
|
||||||
|
handler: 'api/hello.js',
|
||||||
|
runtime: 'node14.x',
|
||||||
|
shouldAddHelpers: true,
|
||||||
|
shouldAddSourcemapSupport: false,
|
||||||
|
});
|
||||||
|
expect(lambda.handler).toEqual('api/hello.js');
|
||||||
|
expect(lambda.runtime).toEqual('node14.x');
|
||||||
|
expect(lambda.shouldAddHelpers).toEqual(true);
|
||||||
|
expect(lambda.shouldAddSourcemapSupport).toEqual(false);
|
||||||
|
expect(lambda.awsLambdaHandler).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vercel",
|
"name": "vercel",
|
||||||
"version": "23.1.3-canary.74",
|
"version": "24.0.0",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "The command-line interface for Vercel",
|
"description": "The command-line interface for Vercel",
|
||||||
@@ -43,14 +43,12 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/build-utils": "2.13.1-canary.1",
|
"@vercel/build-utils": "2.14.0",
|
||||||
"@vercel/go": "1.2.4-canary.6",
|
"@vercel/go": "1.3.0",
|
||||||
"@vercel/node": "1.12.2-canary.9",
|
"@vercel/node": "1.13.0",
|
||||||
"@vercel/python": "2.1.2-canary.4",
|
"@vercel/python": "2.2.0",
|
||||||
"@vercel/ruby": "1.2.10-canary.2",
|
"@vercel/ruby": "1.3.0",
|
||||||
"update-notifier": "4.1.0",
|
"update-notifier": "4.1.0"
|
||||||
"vercel-plugin-middleware": "0.0.0-canary.26",
|
|
||||||
"vercel-plugin-node": "1.12.2-canary.41"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/env": "11.1.2",
|
"@next/env": "11.1.2",
|
||||||
@@ -90,11 +88,11 @@
|
|||||||
"@types/update-notifier": "5.1.0",
|
"@types/update-notifier": "5.1.0",
|
||||||
"@types/which": "1.3.2",
|
"@types/which": "1.3.2",
|
||||||
"@types/write-json-file": "2.2.1",
|
"@types/write-json-file": "2.2.1",
|
||||||
"@vercel/client": "10.2.3-canary.52",
|
"@vercel/client": "10.3.0",
|
||||||
"@vercel/fetch-retry": "5.0.3",
|
"@vercel/fetch-retry": "5.0.3",
|
||||||
"@vercel/frameworks": "0.5.1-canary.21",
|
"@vercel/frameworks": "0.6.0",
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"@vercel/nft": "0.17.0",
|
"@vercel/nft": "0.17.5",
|
||||||
"@zeit/fun": "0.11.2",
|
"@zeit/fun": "0.11.2",
|
||||||
"@zeit/source-map-support": "0.6.2",
|
"@zeit/source-map-support": "0.6.2",
|
||||||
"ajv": "6.12.2",
|
"ajv": "6.12.2",
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ const help = () => {
|
|||||||
|
|
||||||
${chalk.cyan(`$ ${pkgName} bisect --bad example-310pce9i0.vercel.app`)}
|
${chalk.cyan(`$ ${pkgName} bisect --bad example-310pce9i0.vercel.app`)}
|
||||||
|
|
||||||
${chalk.gray('–')} Bisect specifying a deployment that was working 3 days ago
|
|
||||||
|
|
||||||
${chalk.cyan(`$ ${pkgName} bisect --good 3d`)}
|
|
||||||
|
|
||||||
${chalk.gray('–')} Automated bisect with a run script
|
${chalk.gray('–')} Automated bisect with a run script
|
||||||
|
|
||||||
${chalk.cyan(`$ ${pkgName} bisect --run ./test.sh`)}
|
${chalk.cyan(`$ ${pkgName} bisect --run ./test.sh`)}
|
||||||
@@ -201,7 +197,11 @@ export default async function main(client: Client): Promise<number> {
|
|||||||
|
|
||||||
if (badDeployment.target !== goodDeployment.target) {
|
if (badDeployment.target !== goodDeployment.target) {
|
||||||
output.error(
|
output.error(
|
||||||
`Bad deployment target "${badDeployment.target || 'preview'}" does not match good deployment target "${goodDeployment.target || 'preview'}"`
|
`Bad deployment target "${
|
||||||
|
badDeployment.target || 'preview'
|
||||||
|
}" does not match good deployment target "${
|
||||||
|
goodDeployment.target || 'preview'
|
||||||
|
}"`
|
||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,911 +0,0 @@
|
|||||||
import { loadEnvConfig, processEnv } from '@next/env';
|
|
||||||
import {
|
|
||||||
execCommand,
|
|
||||||
getScriptName,
|
|
||||||
GlobOptions,
|
|
||||||
scanParentDirs,
|
|
||||||
spawnAsync,
|
|
||||||
glob as buildUtilsGlob,
|
|
||||||
detectFileSystemAPI,
|
|
||||||
detectBuilders,
|
|
||||||
PackageJson,
|
|
||||||
} from '@vercel/build-utils';
|
|
||||||
import { nodeFileTrace } from '@vercel/nft';
|
|
||||||
import Sema from 'async-sema';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { SpawnOptions } from 'child_process';
|
|
||||||
import { assert } from 'console';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import ogGlob from 'glob';
|
|
||||||
import { dirname, isAbsolute, join, parse, relative } from 'path';
|
|
||||||
import pluralize from 'pluralize';
|
|
||||||
import Client from '../util/client';
|
|
||||||
import { VercelConfig } from '../util/dev/types';
|
|
||||||
import { emoji, prependEmoji } from '../util/emoji';
|
|
||||||
import { CantParseJSONFile } from '../util/errors-ts';
|
|
||||||
import getArgs from '../util/get-args';
|
|
||||||
import handleError from '../util/handle-error';
|
|
||||||
import confirm from '../util/input/confirm';
|
|
||||||
import { isSettingValue } from '../util/is-setting-value';
|
|
||||||
import cmd from '../util/output/cmd';
|
|
||||||
import logo from '../util/output/logo';
|
|
||||||
import param from '../util/output/param';
|
|
||||||
import stamp from '../util/output/stamp';
|
|
||||||
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 { readProjectSettings } from '../util/projects/project-settings';
|
|
||||||
import readJSONFile from '../util/read-json-file';
|
|
||||||
import pull from './pull';
|
|
||||||
|
|
||||||
const sema = new Sema(16, {
|
|
||||||
capacity: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const help = () => {
|
|
||||||
return console.log(`
|
|
||||||
${chalk.bold(`${logo} ${getPkgName()} build`)}
|
|
||||||
|
|
||||||
${chalk.dim('Options:')}
|
|
||||||
|
|
||||||
-h, --help Output usage information
|
|
||||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
|
||||||
'FILE'
|
|
||||||
)} Path to the local ${'`vercel.json`'} file
|
|
||||||
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
|
|
||||||
'DIR'
|
|
||||||
)} Path to the global ${'`.vercel`'} directory
|
|
||||||
--cwd [path] The current working directory
|
|
||||||
-d, --debug Debug mode [off]
|
|
||||||
-y, --yes Skip the confirmation prompt
|
|
||||||
|
|
||||||
${chalk.dim('Examples:')}
|
|
||||||
|
|
||||||
${chalk.gray('–')} Build the project
|
|
||||||
|
|
||||||
${chalk.cyan(`$ ${getPkgName()} build`)}
|
|
||||||
${chalk.cyan(`$ ${getPkgName()} build --cwd ./path-to-project`)}
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OUTPUT_DIR = '.output';
|
|
||||||
|
|
||||||
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 {
|
|
||||||
argv = getArgs(client.argv.slice(2), {
|
|
||||||
'--debug': Boolean,
|
|
||||||
'--cwd': String,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
handleError(err);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv['--help']) {
|
|
||||||
help();
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cwd = argv['--cwd'] || process.cwd();
|
|
||||||
|
|
||||||
let project = await readProjectSettings(join(cwd, VERCEL_DIR));
|
|
||||||
// If there are no project settings, only then do we pull them down
|
|
||||||
while (!project?.settings) {
|
|
||||||
const confirmed = await confirm(
|
|
||||||
`No Project Settings found locally. Run ${getCommandName(
|
|
||||||
'pull'
|
|
||||||
)} for retrieving them?`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (!confirmed) {
|
|
||||||
client.output.print(`Aborted. No Project Settings retrieved.\n`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const result = await pull(client);
|
|
||||||
if (result !== 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Load the environment
|
|
||||||
const { combinedEnv, loadedEnvFiles } = loadEnvConfig(cwd, false, {
|
|
||||||
info: () => ({}), // we don't want to log this yet.
|
|
||||||
error: (...args: any[]) => client.output.error(args.join(' ')),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set process.env with loaded environment variables
|
|
||||||
processEnv(loadedEnvFiles);
|
|
||||||
|
|
||||||
const spawnOpts: {
|
|
||||||
env: Record<string, string | undefined>;
|
|
||||||
} = {
|
|
||||||
env: { ...combinedEnv, VERCEL: '1' },
|
|
||||||
};
|
|
||||||
|
|
||||||
process.chdir(cwd);
|
|
||||||
|
|
||||||
const pkg = await readJSONFile<PackageJson>('./package.json');
|
|
||||||
if (pkg instanceof CantParseJSONFile) {
|
|
||||||
throw pkg;
|
|
||||||
}
|
|
||||||
const vercelConfig = await readJSONFile<VercelConfig>('./vercel.json');
|
|
||||||
if (vercelConfig instanceof CantParseJSONFile) {
|
|
||||||
throw vercelConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.NOW_BUILDER) {
|
|
||||||
// This validation is only necessary when
|
|
||||||
// a user runs `vercel build` locally.
|
|
||||||
const globFiles = await buildUtilsGlob('**', { cwd });
|
|
||||||
const zeroConfig = await detectBuilders(Object.keys(globFiles), pkg);
|
|
||||||
const { reason } = await detectFileSystemAPI({
|
|
||||||
files: globFiles,
|
|
||||||
projectSettings: project.settings,
|
|
||||||
builders: zeroConfig.builders || [],
|
|
||||||
pkg,
|
|
||||||
vercelConfig,
|
|
||||||
tag: '',
|
|
||||||
enableFlag: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reason) {
|
|
||||||
client.output.error(`${cmd(`${getPkgName()} build`)} failed: ${reason}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const framework = findFramework(project.settings.framework);
|
|
||||||
// If this is undefined, we bail. If it is null, then findFramework should return "Other",
|
|
||||||
// so this should really never happen, but just in case....
|
|
||||||
if (framework === undefined) {
|
|
||||||
client.output.error(
|
|
||||||
`Framework detection failed or is malformed. Please run ${getCommandName(
|
|
||||||
'pull'
|
|
||||||
)} again.`
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildState = { ...project.settings };
|
|
||||||
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}`)
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
chalk.dim(
|
|
||||||
formatSetting(
|
|
||||||
'Build Command',
|
|
||||||
project.settings.buildCommand,
|
|
||||||
framework.settings.buildCommand
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
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) {
|
|
||||||
console.log(
|
|
||||||
`Loaded Environment Variables from ${loadedEnvFiles.length} ${pluralize(
|
|
||||||
'file',
|
|
||||||
loadedEnvFiles.length
|
|
||||||
)}:`
|
|
||||||
);
|
|
||||||
for (let envFile of loadedEnvFiles) {
|
|
||||||
console.log(chalk.dim(` - ${envFile.path}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load plugins
|
|
||||||
const debug = argv['--debug'];
|
|
||||||
let plugins;
|
|
||||||
try {
|
|
||||||
plugins = await loadCliPlugins(cwd, client.output);
|
|
||||||
} catch (error) {
|
|
||||||
client.output.error('Failed to load CLI Plugins');
|
|
||||||
handleError(error, { debug });
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origLog = console.log;
|
|
||||||
const origErr = console.error;
|
|
||||||
const prefixedLog = (
|
|
||||||
prefix: string,
|
|
||||||
args: any[],
|
|
||||||
logger: (...args: any[]) => void
|
|
||||||
) => {
|
|
||||||
if (typeof args[0] === 'string') {
|
|
||||||
args[0] = `${prefix} ${args[0]}`;
|
|
||||||
} else {
|
|
||||||
args.unshift(prefix);
|
|
||||||
}
|
|
||||||
return logger(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (plugins?.pluginCount && plugins?.pluginCount > 0) {
|
|
||||||
console.log(
|
|
||||||
`Loaded ${plugins.pluginCount} CLI ${pluralize(
|
|
||||||
'Plugin',
|
|
||||||
plugins.pluginCount
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
// preBuild Plugins
|
|
||||||
if (plugins.preBuildPlugins.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`Running ${plugins.pluginCount} CLI ${pluralize(
|
|
||||||
'Plugin',
|
|
||||||
plugins.pluginCount
|
|
||||||
)} before Build Command:`
|
|
||||||
);
|
|
||||||
for (let item of plugins.preBuildPlugins) {
|
|
||||||
const { name, plugin, color } = item;
|
|
||||||
if (typeof plugin.preBuild === 'function') {
|
|
||||||
const pluginStamp = stamp();
|
|
||||||
const fullName = name + '.preBuild';
|
|
||||||
const prefix = chalk.gray(' > ') + color(fullName + ':');
|
|
||||||
client.output.debug(`Running ${fullName}:`);
|
|
||||||
try {
|
|
||||||
console.log = (...args: any[]) =>
|
|
||||||
prefixedLog(prefix, args, origLog);
|
|
||||||
console.error = (...args: any[]) =>
|
|
||||||
prefixedLog(prefix, args, origErr);
|
|
||||||
await plugin.preBuild();
|
|
||||||
client.output.debug(
|
|
||||||
`Completed ${fullName} ${chalk.dim(`${pluginStamp()}`)}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
client.output.error(`${prefix} failed`);
|
|
||||||
handleError(error, { debug });
|
|
||||||
return 1;
|
|
||||||
} finally {
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origErr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean the output directory
|
|
||||||
fs.removeSync(join(cwd, OUTPUT_DIR));
|
|
||||||
|
|
||||||
if (framework && process.env.VERCEL_URL && 'envPrefix' in framework) {
|
|
||||||
for (const key of Object.keys(process.env)) {
|
|
||||||
if (key.startsWith('VERCEL_')) {
|
|
||||||
const newKey = `${framework.envPrefix}${key}`;
|
|
||||||
// Set `process.env` and `spawnOpts.env` to make sure the variables are
|
|
||||||
// available to the `build` step and the CLI Plugins.
|
|
||||||
process.env[newKey] = process.env[newKey] || process.env[key];
|
|
||||||
spawnOpts.env[newKey] = process.env[newKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required for Next.js to produce the correct `.nft.json` files.
|
|
||||||
spawnOpts.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = baseDir;
|
|
||||||
|
|
||||||
// Yarn v2 PnP mode may be activated, so force
|
|
||||||
// "node-modules" linker style
|
|
||||||
const env = {
|
|
||||||
YARN_NODE_LINKER: 'node-modules',
|
|
||||||
...spawnOpts.env,
|
|
||||||
};
|
|
||||||
|
|
||||||
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(
|
|
||||||
client,
|
|
||||||
cwd,
|
|
||||||
['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))) {
|
|
||||||
let dotNextDir: string | null = null;
|
|
||||||
|
|
||||||
// If a custom `outputDirectory` was set, we'll need to verify
|
|
||||||
// if it's `.next` output, or just static output.
|
|
||||||
const userOutputDirectory = project.settings.outputDirectory;
|
|
||||||
|
|
||||||
if (typeof userOutputDirectory === 'string') {
|
|
||||||
if (fs.existsSync(join(cwd, userOutputDirectory, 'BUILD_ID'))) {
|
|
||||||
dotNextDir = join(cwd, userOutputDirectory);
|
|
||||||
client.output.debug(
|
|
||||||
`Consider ${param(userOutputDirectory)} as ${param('.next')} output.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (fs.existsSync(join(cwd, '.next'))) {
|
|
||||||
dotNextDir = join(cwd, '.next');
|
|
||||||
client.output.debug(`Found ${param('.next')} directory.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We cannot rely on the `framework` alone, as it might be a static export,
|
|
||||||
// and the current build might use a different project that's not in the settings.
|
|
||||||
const isNextOutput = Boolean(dotNextDir);
|
|
||||||
const nextExport = await getNextExportStatus(dotNextDir);
|
|
||||||
const outputDir =
|
|
||||||
isNextOutput && !nextExport ? OUTPUT_DIR : join(OUTPUT_DIR, 'static');
|
|
||||||
const getDistDir = framework.getFsOutputDir || framework.getOutputDirName;
|
|
||||||
const distDir =
|
|
||||||
(nextExport?.exportDetail.outDirectory
|
|
||||||
? relative(cwd, nextExport.exportDetail.outDirectory)
|
|
||||||
: false) ||
|
|
||||||
dotNextDir ||
|
|
||||||
userOutputDirectory ||
|
|
||||||
(await getDistDir(cwd));
|
|
||||||
|
|
||||||
await fs.ensureDir(join(cwd, outputDir));
|
|
||||||
|
|
||||||
const copyStamp = stamp();
|
|
||||||
client.output.spinner(
|
|
||||||
`Copying files from ${param(distDir)} to ${param(outputDir)}`
|
|
||||||
);
|
|
||||||
const files = await glob(join(relative(cwd, distDir), '**'), {
|
|
||||||
ignore: [
|
|
||||||
'node_modules/**',
|
|
||||||
'.vercel/**',
|
|
||||||
'.env',
|
|
||||||
'.env.*',
|
|
||||||
'.*ignore',
|
|
||||||
'_middleware.ts',
|
|
||||||
'_middleware.mts',
|
|
||||||
'_middleware.cts',
|
|
||||||
'_middleware.mjs',
|
|
||||||
'_middleware.cjs',
|
|
||||||
'_middleware.js',
|
|
||||||
'api/**',
|
|
||||||
'.git/**',
|
|
||||||
'.next/cache/**',
|
|
||||||
],
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
cwd,
|
|
||||||
absolute: true,
|
|
||||||
});
|
|
||||||
await Promise.all(
|
|
||||||
files.map(f =>
|
|
||||||
smartCopy(
|
|
||||||
client,
|
|
||||||
f,
|
|
||||||
distDir === '.'
|
|
||||||
? join(cwd, outputDir, relative(cwd, f))
|
|
||||||
: f.replace(distDir, outputDir)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
client.output.stopSpinner();
|
|
||||||
console.log(
|
|
||||||
`Copied ${files.length.toLocaleString()} files from ${param(
|
|
||||||
distDir
|
|
||||||
)} to ${param(outputDir)} ${copyStamp()}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildManifestPath = join(cwd, OUTPUT_DIR, 'build-manifest.json');
|
|
||||||
const routesManifestPath = join(cwd, OUTPUT_DIR, 'routes-manifest.json');
|
|
||||||
|
|
||||||
if (!fs.existsSync(buildManifestPath)) {
|
|
||||||
client.output.debug(
|
|
||||||
`Generating build manifest: ${param(buildManifestPath)}`
|
|
||||||
);
|
|
||||||
const buildManifest = {
|
|
||||||
version: 1,
|
|
||||||
cache: framework.cachePattern ? [framework.cachePattern] : [],
|
|
||||||
};
|
|
||||||
await fs.writeJSON(buildManifestPath, buildManifest, { spaces: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(routesManifestPath)) {
|
|
||||||
client.output.debug(
|
|
||||||
`Generating routes manifest: ${param(routesManifestPath)}`
|
|
||||||
);
|
|
||||||
const routesManifest = {
|
|
||||||
version: 3,
|
|
||||||
pages404: true,
|
|
||||||
basePath: '',
|
|
||||||
redirects: framework.defaultRedirects ?? [],
|
|
||||||
headers: framework.defaultHeaders ?? [],
|
|
||||||
dynamicRoutes: [],
|
|
||||||
dataRoutes: [],
|
|
||||||
rewrites: framework.defaultRewrites ?? [],
|
|
||||||
};
|
|
||||||
await fs.writeJSON(
|
|
||||||
join(cwd, OUTPUT_DIR, 'routes-manifest.json'),
|
|
||||||
routesManifest,
|
|
||||||
{ spaces: 2 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special Next.js processing.
|
|
||||||
if (nextExport) {
|
|
||||||
client.output.debug('Found `next export` output.');
|
|
||||||
|
|
||||||
const htmlFiles = await buildUtilsGlob(
|
|
||||||
'**/*.html',
|
|
||||||
join(cwd, OUTPUT_DIR, 'static')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextExport.exportDetail.success !== true) {
|
|
||||||
client.output.error(
|
|
||||||
`Export of Next.js app failed. Please check your build logs.`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'server', 'pages'));
|
|
||||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'static'));
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(htmlFiles).map(async fileName => {
|
|
||||||
await sema.acquire();
|
|
||||||
|
|
||||||
const input = join(cwd, OUTPUT_DIR, 'static', fileName);
|
|
||||||
const target = join(cwd, OUTPUT_DIR, 'server', 'pages', fileName);
|
|
||||||
|
|
||||||
await fs.mkdirp(dirname(target));
|
|
||||||
|
|
||||||
await fs.promises.rename(input, target).finally(() => {
|
|
||||||
sema.release();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const file of [
|
|
||||||
'BUILD_ID',
|
|
||||||
'images-manifest.json',
|
|
||||||
'routes-manifest.json',
|
|
||||||
'build-manifest.json',
|
|
||||||
]) {
|
|
||||||
const input = join(nextExport.dotNextDir, file);
|
|
||||||
|
|
||||||
if (fs.existsSync(input)) {
|
|
||||||
// Do not use `smartCopy`, since we want to overwrite if they already exist.
|
|
||||||
await fs.copyFile(input, join(OUTPUT_DIR, file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isNextOutput) {
|
|
||||||
// The contents of `.output/static` should be placed inside of `.output/static/_next/static`
|
|
||||||
const tempStatic = '___static';
|
|
||||||
await fs.rename(
|
|
||||||
join(cwd, OUTPUT_DIR, 'static'),
|
|
||||||
join(cwd, OUTPUT_DIR, tempStatic)
|
|
||||||
);
|
|
||||||
await fs.mkdirp(join(cwd, OUTPUT_DIR, 'static', '_next', 'static'));
|
|
||||||
await fs.rename(
|
|
||||||
join(cwd, OUTPUT_DIR, tempStatic),
|
|
||||||
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
|
|
||||||
// allow `static`, and newer ones allow both, but since there's nobody that actually uses both,
|
|
||||||
// we can check for the existence of both and pick the first match that we find (first
|
|
||||||
// `public`, then`static`). We can't read both at the same time because that would mean we'd
|
|
||||||
// read public for old Next.js versions that don't support it, which might be breaking (and
|
|
||||||
// we don't want to make vercel build specific framework versions).
|
|
||||||
const nextSrcDirectory = dirname(distDir);
|
|
||||||
|
|
||||||
const publicFiles = await glob('public/**', {
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
cwd: nextSrcDirectory,
|
|
||||||
absolute: true,
|
|
||||||
});
|
|
||||||
if (publicFiles.length > 0) {
|
|
||||||
await Promise.all(
|
|
||||||
publicFiles.map(f =>
|
|
||||||
smartCopy(
|
|
||||||
client,
|
|
||||||
f,
|
|
||||||
join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
'static',
|
|
||||||
relative(join(dirname(distDir), 'public'), f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const staticFiles = await glob('static/**', {
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
cwd: nextSrcDirectory,
|
|
||||||
absolute: true,
|
|
||||||
});
|
|
||||||
await Promise.all(
|
|
||||||
staticFiles.map(f =>
|
|
||||||
smartCopy(
|
|
||||||
client,
|
|
||||||
f,
|
|
||||||
join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
'static',
|
|
||||||
'static',
|
|
||||||
relative(join(dirname(distDir), 'static'), f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regardless of the Next.js version, we make sure that it is compatible with
|
|
||||||
// the Filesystem API. We get there by moving all the files needed
|
|
||||||
// into the outputs directory `inputs` folder. Next.js is > 12, we can
|
|
||||||
// read the .nft.json files directly. If there aren't .nft.json files
|
|
||||||
// we trace and create them. We then resolve the files in each nft file list
|
|
||||||
// and move them into the "inputs" directory. We rename them with hashes to
|
|
||||||
// prevent collisions and then update the related .nft files accordingly
|
|
||||||
// to point to the newly named input files. Again, all of this is so that Next.js
|
|
||||||
// works with the Filesystem API (and so .output contains all inputs
|
|
||||||
// needed to run Next.js) and `vc --prebuilt`.
|
|
||||||
const nftFiles = await glob(join(OUTPUT_DIR, '**', '*.nft.json'), {
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
ignore: ['cache/**'],
|
|
||||||
cwd,
|
|
||||||
absolute: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there are no .nft.json files, we know that Next.js < 12. We then
|
|
||||||
// execute the tracing on our own.
|
|
||||||
if (nftFiles.length === 0) {
|
|
||||||
const serverFiles = await glob(
|
|
||||||
join(OUTPUT_DIR, 'server', 'pages', '**', '*.js'),
|
|
||||||
{
|
|
||||||
nodir: true,
|
|
||||||
dot: true,
|
|
||||||
cwd,
|
|
||||||
ignore: ['webpack-runtime.js'],
|
|
||||||
absolute: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
for (let f of serverFiles) {
|
|
||||||
const { ext, dir } = parse(f);
|
|
||||||
const { fileList } = await nodeFileTrace([f], {
|
|
||||||
ignore: [
|
|
||||||
relative(cwd, f),
|
|
||||||
'node_modules/next/dist/pages/**/*',
|
|
||||||
'node_modules/next/dist/compiled/webpack/(bundle4|bundle5).js',
|
|
||||||
'node_modules/react/**/*.development.js',
|
|
||||||
'node_modules/react-dom/**/*.development.js',
|
|
||||||
'node_modules/use-subscription/**/*.development.js',
|
|
||||||
'node_modules/sharp/**/*',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
fileList.delete(relative(cwd, f));
|
|
||||||
|
|
||||||
const nftFileName = f.replace(ext, '.js.nft.json');
|
|
||||||
client.output.debug(`Creating ${nftFileName}`);
|
|
||||||
|
|
||||||
await fs.writeJSON(nftFileName, {
|
|
||||||
version: 2,
|
|
||||||
files: Array.from(fileList).map(fileListEntry =>
|
|
||||||
relative(dir, fileListEntry)
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredServerFilesPath = join(
|
|
||||||
OUTPUT_DIR,
|
|
||||||
'required-server-files.json'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(requiredServerFilesPath)) {
|
|
||||||
client.output.debug(`Resolve ${param('required-server-files.json')}.`);
|
|
||||||
|
|
||||||
const requiredServerFilesJson = await fs.readJSON(
|
|
||||||
requiredServerFilesPath
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.writeJSON(requiredServerFilesPath, {
|
|
||||||
...requiredServerFilesJson,
|
|
||||||
appDir: '.',
|
|
||||||
files: requiredServerFilesJson.files.map((i: string) => {
|
|
||||||
const originalPath = join(requiredServerFilesJson.appDir, i);
|
|
||||||
const relPath = join(OUTPUT_DIR, relative(distDir, originalPath));
|
|
||||||
|
|
||||||
return relPath;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Plugins
|
|
||||||
if (plugins?.buildPlugins && plugins.buildPlugins.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`Running ${plugins.pluginCount} CLI ${pluralize(
|
|
||||||
'Plugin',
|
|
||||||
plugins.pluginCount
|
|
||||||
)} after Build Command:`
|
|
||||||
);
|
|
||||||
let vercelConfig: VercelConfig = {};
|
|
||||||
try {
|
|
||||||
vercelConfig = await fs.readJSON(join(cwd, 'vercel.json'));
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw new Error(`Failed to read vercel.json: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let item of plugins.buildPlugins) {
|
|
||||||
const { name, plugin, color } = item;
|
|
||||||
if (typeof plugin.build === 'function') {
|
|
||||||
const pluginStamp = stamp();
|
|
||||||
const fullName = name + '.build';
|
|
||||||
const prefix = chalk.gray(' > ') + color(fullName + ':');
|
|
||||||
client.output.debug(`Running ${fullName}:`);
|
|
||||||
try {
|
|
||||||
console.log = (...args: any[]) => prefixedLog(prefix, args, origLog);
|
|
||||||
console.error = (...args: any[]) =>
|
|
||||||
prefixedLog(prefix, args, origErr);
|
|
||||||
await plugin.build({
|
|
||||||
vercelConfig,
|
|
||||||
workPath: cwd,
|
|
||||||
});
|
|
||||||
client.output.debug(
|
|
||||||
`Completed ${fullName} ${chalk.dim(`${pluginStamp()}`)}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
client.output.error(`${prefix} failed`);
|
|
||||||
handleError(error, { debug });
|
|
||||||
return 1;
|
|
||||||
} finally {
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${prependEmoji(
|
|
||||||
`Build Completed in ${chalk.bold(OUTPUT_DIR)} ${chalk.gray(
|
|
||||||
buildStamp()
|
|
||||||
)}`,
|
|
||||||
emoji('success')
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runPackageJsonScript(
|
|
||||||
client: Client,
|
|
||||||
destPath: string,
|
|
||||||
scriptNames: string | Iterable<string>,
|
|
||||||
spawnOpts?: SpawnOptions
|
|
||||||
) {
|
|
||||||
assert(isAbsolute(destPath));
|
|
||||||
|
|
||||||
const { packageJson, cliType, lockfileVersion } = await scanParentDirs(
|
|
||||||
destPath,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const scriptName = getScriptName(
|
|
||||||
packageJson,
|
|
||||||
typeof scriptNames === 'string' ? [scriptNames] : scriptNames
|
|
||||||
);
|
|
||||||
if (!scriptName) return false;
|
|
||||||
|
|
||||||
client.output.debug('Running user script...');
|
|
||||||
const runScriptTime = Date.now();
|
|
||||||
|
|
||||||
const opts: any = { cwd: destPath, ...spawnOpts };
|
|
||||||
const env = (opts.env = { ...process.env, ...opts.env });
|
|
||||||
|
|
||||||
if (cliType === 'npm') {
|
|
||||||
opts.prettyCommand = `npm run ${scriptName}`;
|
|
||||||
|
|
||||||
if (typeof lockfileVersion === 'number' && lockfileVersion >= 2) {
|
|
||||||
// Ensure that npm 7 is at the beginning of the `$PATH`
|
|
||||||
env.PATH = `/node16/bin-npm7:${env.PATH}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
opts.prettyCommand = `yarn run ${scriptName}`;
|
|
||||||
|
|
||||||
// Yarn v2 PnP mode may be activated, so force "node-modules" linker style
|
|
||||||
if (!env.YARN_NODE_LINKER) {
|
|
||||||
env.YARN_NODE_LINKER = 'node-modules';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Running Build Command: ${cmd(opts.prettyCommand)}\n`);
|
|
||||||
await spawnAsync(cliType, ['run', scriptName], opts);
|
|
||||||
console.log(); // give it some room
|
|
||||||
client.output.debug(`Script complete [${Date.now() - runScriptTime}ms]`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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), 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.copy(existingPath, newPath, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function smartCopy(client: Client, from: string, to: string) {
|
|
||||||
sema.acquire();
|
|
||||||
try {
|
|
||||||
client.output.debug(`Copying from ${from} to ${to}`);
|
|
||||||
await linkOrCopy(from, to);
|
|
||||||
} finally {
|
|
||||||
sema.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function glob(pattern: string, options: GlobOptions): Promise<string[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ogGlob(pattern, options, (err, files) => {
|
|
||||||
err ? reject(err) : resolve(files);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Files will only exist when `next export` was used.
|
|
||||||
*/
|
|
||||||
async function getNextExportStatus(dotNextDir: string | null) {
|
|
||||||
if (!dotNextDir) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportDetail: {
|
|
||||||
success: boolean;
|
|
||||||
outDirectory: string;
|
|
||||||
} | null = await fs
|
|
||||||
.readJson(join(dotNextDir, 'export-detail.json'))
|
|
||||||
.catch(error => {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!exportDetail) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportMarker: {
|
|
||||||
version: 1;
|
|
||||||
exportTrailingSlash: boolean;
|
|
||||||
hasExportPathMap: boolean;
|
|
||||||
} | null = await fs
|
|
||||||
.readJSON(join(dotNextDir, 'export-marker.json'))
|
|
||||||
.catch(error => {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
dotNextDir,
|
|
||||||
exportDetail,
|
|
||||||
exportMarker: {
|
|
||||||
trailingSlash: exportMarker?.hasExportPathMap
|
|
||||||
? exportMarker.exportTrailingSlash
|
|
||||||
: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ export default new Map([
|
|||||||
['aliases', 'alias'],
|
['aliases', 'alias'],
|
||||||
['billing', 'billing'],
|
['billing', 'billing'],
|
||||||
['bisect', 'bisect'],
|
['bisect', 'bisect'],
|
||||||
['build', 'build'],
|
|
||||||
['cc', 'billing'],
|
['cc', 'billing'],
|
||||||
['cert', 'certs'],
|
['cert', 'certs'],
|
||||||
['certs', 'certs'],
|
['certs', 'certs'],
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ const main = async () => {
|
|||||||
// * a subcommand (as in: `vercel ls`)
|
// * a subcommand (as in: `vercel ls`)
|
||||||
const targetOrSubcommand = argv._[2];
|
const targetOrSubcommand = argv._[2];
|
||||||
|
|
||||||
if (targetOrSubcommand === 'build') {
|
const betaCommands: string[] = [];
|
||||||
|
if (betaCommands.includes(targetOrSubcommand)) {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.grey(
|
`${chalk.grey(
|
||||||
`${getTitleName()} CLI ${
|
`${getTitleName()} CLI ${
|
||||||
@@ -292,14 +293,7 @@ const main = async () => {
|
|||||||
|
|
||||||
let authConfig = null;
|
let authConfig = null;
|
||||||
|
|
||||||
const subcommandsWithoutToken = [
|
const subcommandsWithoutToken = ['login', 'logout', 'help', 'init', 'update'];
|
||||||
'login',
|
|
||||||
'logout',
|
|
||||||
'help',
|
|
||||||
'init',
|
|
||||||
'update',
|
|
||||||
'build',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (authConfigExists) {
|
if (authConfigExists) {
|
||||||
try {
|
try {
|
||||||
@@ -406,33 +400,20 @@ const main = async () => {
|
|||||||
} else if (commands.has(singular)) {
|
} else if (commands.has(singular)) {
|
||||||
alternative = singular;
|
alternative = singular;
|
||||||
}
|
}
|
||||||
if (targetOrSubcommand === 'build') {
|
console.error(
|
||||||
output.note(
|
error(
|
||||||
`If you wish to deploy the ${fileType} ${param(
|
`The supplied argument ${param(targetOrSubcommand)} is ambiguous.` +
|
||||||
targetOrSubcommand
|
`\nIf you wish to deploy the ${fileType} ${param(
|
||||||
)}, run ${getCommandName('deploy build')}.` +
|
targetOrSubcommand
|
||||||
|
)}, first run "cd ${targetOrSubcommand}". ` +
|
||||||
(alternative
|
(alternative
|
||||||
? `\nIf you wish to use the subcommand ${param(
|
? `\nIf you wish to use the subcommand ${param(
|
||||||
targetOrSubcommand
|
targetOrSubcommand
|
||||||
)}, use ${param(alternative)} instead.`
|
)}, use ${param(alternative)} instead.`
|
||||||
: '')
|
: '')
|
||||||
);
|
)
|
||||||
} else {
|
);
|
||||||
console.error(
|
return 1;
|
||||||
error(
|
|
||||||
`The supplied argument ${param(targetOrSubcommand)} is ambiguous.` +
|
|
||||||
`\nIf you wish to deploy the ${fileType} ${param(
|
|
||||||
targetOrSubcommand
|
|
||||||
)}, first run "cd ${targetOrSubcommand}". ` +
|
|
||||||
(alternative
|
|
||||||
? `\nIf you wish to use the subcommand ${param(
|
|
||||||
targetOrSubcommand
|
|
||||||
)}, use ${param(alternative)} instead.`
|
|
||||||
: '')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommandExists) {
|
if (subcommandExists) {
|
||||||
@@ -630,9 +611,6 @@ const main = async () => {
|
|||||||
case 'bisect':
|
case 'bisect':
|
||||||
func = await import('./commands/bisect');
|
func = await import('./commands/bisect');
|
||||||
break;
|
break;
|
||||||
case 'build':
|
|
||||||
func = await import('./commands/build');
|
|
||||||
break;
|
|
||||||
case 'certs':
|
case 'certs':
|
||||||
func = await import('./commands/certs');
|
func = await import('./commands/certs');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
detectApiExtensions,
|
detectApiExtensions,
|
||||||
spawnCommand,
|
spawnCommand,
|
||||||
isOfficialRuntime,
|
isOfficialRuntime,
|
||||||
detectFileSystemAPI,
|
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import frameworkList from '@vercel/frameworks';
|
import frameworkList from '@vercel/frameworks';
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||||
import exposeSystemEnvs from './expose-system-envs';
|
import exposeSystemEnvs from './expose-system-envs';
|
||||||
import { loadCliPlugins } from '../plugins';
|
|
||||||
|
|
||||||
const frontendRuntimeSet = new Set(
|
const frontendRuntimeSet = new Set(
|
||||||
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
|
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
|
||||||
@@ -600,32 +598,6 @@ export default class DevServer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { reason, metadata } = await detectFileSystemAPI({
|
|
||||||
files,
|
|
||||||
builders: builders || [],
|
|
||||||
projectSettings: projectSettings || this.projectSettings || {},
|
|
||||||
vercelConfig,
|
|
||||||
pkg,
|
|
||||||
tag: '',
|
|
||||||
enableFlag: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reason) {
|
|
||||||
if (metadata.hasMiddleware) {
|
|
||||||
this.output.error(
|
|
||||||
`Detected middleware usage which requires the latest API. ${reason}`
|
|
||||||
);
|
|
||||||
await this.exit();
|
|
||||||
} else if (metadata.plugins.length > 0) {
|
|
||||||
this.output.error(
|
|
||||||
`Detected CLI plugins which requires the latest API. ${reason}`
|
|
||||||
);
|
|
||||||
await this.exit();
|
|
||||||
} else {
|
|
||||||
this.output.warn(`Unable to use latest API. ${reason}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builders) {
|
if (builders) {
|
||||||
if (this.devCommand) {
|
if (this.devCommand) {
|
||||||
builders = builders.filter(filterFrontendBuilds);
|
builders = builders.filter(filterFrontendBuilds);
|
||||||
@@ -1377,6 +1349,7 @@ export default class DevServer {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
runDevMiddleware = async (
|
runDevMiddleware = async (
|
||||||
req: http.IncomingMessage,
|
req: http.IncomingMessage,
|
||||||
res: http.ServerResponse
|
res: http.ServerResponse
|
||||||
@@ -1400,6 +1373,7 @@ export default class DevServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve project directory as a v2 deployment.
|
* Serve project directory as a v2 deployment.
|
||||||
@@ -1468,6 +1442,7 @@ export default class DevServer {
|
|||||||
let prevUrl = req.url;
|
let prevUrl = req.url;
|
||||||
let prevHeaders: HttpHeadersConfig = {};
|
let prevHeaders: HttpHeadersConfig = {};
|
||||||
|
|
||||||
|
/*
|
||||||
const middlewareResult = await this.runDevMiddleware(req, res);
|
const middlewareResult = await this.runDevMiddleware(req, res);
|
||||||
|
|
||||||
if (middlewareResult) {
|
if (middlewareResult) {
|
||||||
@@ -1497,6 +1472,7 @@ export default class DevServer {
|
|||||||
prevUrl = url.format(origUrl);
|
prevUrl = url.format(origUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
for (const phase of phases) {
|
for (const phase of phases) {
|
||||||
statusCode = undefined;
|
statusCode = undefined;
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ const readFile = promisify(fs.readFile);
|
|||||||
const writeFile = promisify(fs.writeFile);
|
const writeFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
export const VERCEL_DIR = '.vercel';
|
export const VERCEL_DIR = '.vercel';
|
||||||
export const VERCEL_OUTPUT_DIR = '.output';
|
|
||||||
export const VERCEL_DIR_FALLBACK = '.now';
|
export const VERCEL_DIR_FALLBACK = '.now';
|
||||||
export const VERCEL_DIR_README = 'README.txt';
|
export const VERCEL_DIR_README = 'README.txt';
|
||||||
export const VERCEL_DIR_PROJECT = 'project.json';
|
export const VERCEL_DIR_PROJECT = 'project.json';
|
||||||
@@ -256,13 +255,6 @@ export async function linkFolderToProject(
|
|||||||
contentModified = true;
|
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) {
|
if (contentModified) {
|
||||||
await writeFile(gitIgnorePath, gitIgnore);
|
await writeFile(gitIgnorePath, gitIgnore);
|
||||||
isGitIgnoreUpdated = true;
|
isGitIgnoreUpdated = true;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.next
|
.next
|
||||||
yarn.lock
|
yarn.lock
|
||||||
.vercel
|
.vercel
|
||||||
.output
|
|
||||||
@@ -367,7 +367,7 @@ module.exports = async function prepare(session, binaryPath) {
|
|||||||
},
|
},
|
||||||
'project-link-gitignore': {
|
'project-link-gitignore': {
|
||||||
'package.json': '{}',
|
'package.json': '{}',
|
||||||
'.gitignore': '.output',
|
'.gitignore': '',
|
||||||
},
|
},
|
||||||
'project-link-legacy': {
|
'project-link-legacy': {
|
||||||
'index.html': 'Hello',
|
'index.html': 'Hello',
|
||||||
|
|||||||
68
packages/cli/test/integration.js
vendored
68
packages/cli/test/integration.js
vendored
@@ -2271,62 +2271,6 @@ test('[vercel dev] fails when development commad calls vercel dev recursively',
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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}"`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('`vercel rm` removes a deployment', async t => {
|
test('`vercel rm` removes a deployment', async t => {
|
||||||
const directory = fixture('static-deployment');
|
const directory = fixture('static-deployment');
|
||||||
|
|
||||||
@@ -2784,7 +2728,7 @@ test('should show prompts to set up project during first deploy', async t => {
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.vercel\n.output\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
|
|
||||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||||
t.is(
|
t.is(
|
||||||
@@ -3353,7 +3297,7 @@ test('[vc link] should show prompts to set up project', async t => {
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.vercel\n.output\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
|
|
||||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||||
t.is(
|
t.is(
|
||||||
@@ -3388,7 +3332,7 @@ test('[vc link --confirm] should not show prompts and autolink', async t => {
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.vercel\n.output\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
|
|
||||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||||
t.is(
|
t.is(
|
||||||
@@ -3423,7 +3367,7 @@ test('[vc link] should not duplicate paths in .gitignore', async t => {
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.output\n.vercel\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[vc dev] should show prompts to set up project', async t => {
|
test('[vc dev] should show prompts to set up project', async t => {
|
||||||
@@ -3447,7 +3391,7 @@ test('[vc dev] should show prompts to set up project', async t => {
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.vercel\n.output\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
|
|
||||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||||
t.is(
|
t.is(
|
||||||
@@ -3514,7 +3458,7 @@ test('[vc link] should show project prompts but not framework when `builds` defi
|
|||||||
|
|
||||||
// Ensure .gitignore is created
|
// Ensure .gitignore is created
|
||||||
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
const gitignore = await readFile(path.join(dir, '.gitignore'), 'utf8');
|
||||||
t.is(gitignore, '.vercel\n.output\n');
|
t.is(gitignore, '.vercel\n');
|
||||||
|
|
||||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||||
t.is(
|
t.is(
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ describe('DevServer', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
it(
|
it(
|
||||||
'should support edge middleware',
|
'should support edge middleware',
|
||||||
testFixture('edge-middleware', async server => {
|
testFixture('edge-middleware', async server => {
|
||||||
@@ -394,4 +395,5 @@ describe('DevServer', () => {
|
|||||||
expect(body).toStrictEqual('is strict mode? yes');
|
expect(body).toStrictEqual('is strict mode? yes');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/client",
|
"name": "@vercel/client",
|
||||||
"version": "10.2.3-canary.52",
|
"version": "10.3.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"typings": "dist/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"homepage": "https://vercel.com",
|
"homepage": "https://vercel.com",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/build-utils": "2.13.1-canary.1",
|
"@vercel/build-utils": "2.14.0",
|
||||||
"@zeit/fetch": "5.2.0",
|
"@zeit/fetch": "5.2.0",
|
||||||
"async-retry": "1.2.3",
|
"async-retry": "1.2.3",
|
||||||
"async-sema": "3.0.0",
|
"async-sema": "3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/frameworks",
|
"name": "@vercel/frameworks",
|
||||||
"version": "0.5.1-canary.21",
|
"version": "0.6.0",
|
||||||
"main": "./dist/frameworks.js",
|
"main": "./dist/frameworks.js",
|
||||||
"types": "./dist/frameworks.d.ts",
|
"types": "./dist/frameworks.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"@types/js-yaml": "3.12.1",
|
"@types/js-yaml": "3.12.1",
|
||||||
"@types/node": "12.0.4",
|
"@types/node": "12.0.4",
|
||||||
"@types/node-fetch": "2.5.8",
|
"@types/node-fetch": "2.5.8",
|
||||||
"@vercel/routing-utils": "1.11.4-canary.6",
|
"@vercel/routing-utils": "1.12.0",
|
||||||
"ajv": "6.12.2",
|
"ajv": "6.12.2",
|
||||||
"typescript": "4.3.4"
|
"typescript": "4.3.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/go",
|
"name": "@vercel/go",
|
||||||
"version": "1.2.4-canary.6",
|
"version": "1.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index",
|
"main": "./dist/index",
|
||||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"@types/fs-extra": "^5.0.5",
|
"@types/fs-extra": "^5.0.5",
|
||||||
"@types/node-fetch": "^2.3.0",
|
"@types/node-fetch": "^2.3.0",
|
||||||
"@types/tar": "^4.0.0",
|
"@types/tar": "^4.0.0",
|
||||||
"@vercel/build-utils": "2.13.1-canary.1",
|
"@vercel/build-utils": "2.14.0",
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"async-retry": "1.3.1",
|
"async-retry": "1.3.1",
|
||||||
"execa": "^1.0.0",
|
"execa": "^1.0.0",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
entries.js
|
|
||||||
dist
|
|
||||||
2
packages/middleware/.gitignore
vendored
2
packages/middleware/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/dist
|
|
||||||
/test/fixtures/*/.output
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/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');
|
|
||||||
|
|
||||||
// Start fresh
|
|
||||||
await fs.remove(outDir);
|
|
||||||
|
|
||||||
await execa(
|
|
||||||
'ncc',
|
|
||||||
['build', join(srcDir, 'index.ts'), '-o', outDir, '--external', 'esbuild'],
|
|
||||||
{
|
|
||||||
stdio: 'inherit',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.copyFile(
|
|
||||||
join(__dirname, 'src/entries.js'),
|
|
||||||
join(outDir, 'entries.js')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "vercel-plugin-middleware",
|
|
||||||
"version": "0.0.0-canary.26",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./dist/index",
|
|
||||||
"homepage": "",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/vercel/vercel.git",
|
|
||||||
"directory": "packages/middleware"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "node build",
|
|
||||||
"test-unit": "jest",
|
|
||||||
"prepublishOnly": "node build"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"esbuild": "0.13.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@peculiar/webcrypto": "1.2.0",
|
|
||||||
"@types/cookie": "0.4.1",
|
|
||||||
"@types/glob": "7.2.0",
|
|
||||||
"@types/http-proxy": "1.17.7",
|
|
||||||
"@types/jest": "27.0.2",
|
|
||||||
"@types/node": "16.11.6",
|
|
||||||
"@types/node-fetch": "^2",
|
|
||||||
"@types/ua-parser-js": "0.7.36",
|
|
||||||
"@types/uuid": "8.3.1",
|
|
||||||
"@vercel/build-utils": "2.13.1-canary.1",
|
|
||||||
"@vercel/ncc": "0.24.0",
|
|
||||||
"cookie": "0.4.1",
|
|
||||||
"formdata-node": "4.3.1",
|
|
||||||
"glob": "7.2.0",
|
|
||||||
"http-proxy": "1.18.1",
|
|
||||||
"node-fetch": "^2",
|
|
||||||
"ua-parser-js": "1.0.2",
|
|
||||||
"url": "0.11.0",
|
|
||||||
"uuid": "8.3.2",
|
|
||||||
"web-streams-polyfill": "3.1.1"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"preset": "ts-jest",
|
|
||||||
"globals": {
|
|
||||||
"ts-jest": {
|
|
||||||
"diagnostics": false,
|
|
||||||
"isolatedModules": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"verbose": false,
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testMatch": [
|
|
||||||
"<rootDir>/test/**/*.test.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import * as middleware from './_temp_middleware';
|
|
||||||
_ENTRIES = typeof _ENTRIES === 'undefined' ? {} : _ENTRIES;
|
|
||||||
_ENTRIES['middleware_pages/_middleware'] = {
|
|
||||||
default: async function (ev) {
|
|
||||||
const result = await middleware.default(ev.request, ev);
|
|
||||||
return {
|
|
||||||
promise: Promise.resolve(),
|
|
||||||
waitUntil: Promise.resolve(),
|
|
||||||
response:
|
|
||||||
result ||
|
|
||||||
new Response(null, {
|
|
||||||
headers: {
|
|
||||||
'x-middleware-next': 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import * as esbuild from 'esbuild';
|
|
||||||
|
|
||||||
const processInjectFile = `
|
|
||||||
// envOverride is passed by esbuild plugin
|
|
||||||
const env = envOverride
|
|
||||||
function cwd() {
|
|
||||||
return '/'
|
|
||||||
}
|
|
||||||
function chdir(dir) {
|
|
||||||
throw new Error('process.chdir is not supported')
|
|
||||||
}
|
|
||||||
export const process = {
|
|
||||||
argv: [],
|
|
||||||
env,
|
|
||||||
chdir,
|
|
||||||
cwd,
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function nodeProcessPolyfillPlugin({ env = {} } = {}): esbuild.Plugin {
|
|
||||||
return {
|
|
||||||
name: 'node-process-polyfill',
|
|
||||||
setup({ initialOptions, onResolve, onLoad }) {
|
|
||||||
onResolve({ filter: /_virtual-process-polyfill_\.js/ }, ({ path }) => {
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
sideEffects: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
onLoad({ filter: /_virtual-process-polyfill_\.js/ }, () => {
|
|
||||||
const contents = `const envOverride = ${JSON.stringify(
|
|
||||||
env
|
|
||||||
)};\n${processInjectFile}`;
|
|
||||||
return {
|
|
||||||
loader: 'js',
|
|
||||||
contents,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const polyfills = [
|
|
||||||
path.resolve(__dirname, '_virtual-process-polyfill_.js'),
|
|
||||||
];
|
|
||||||
if (initialOptions.inject) {
|
|
||||||
initialOptions.inject.push(...polyfills);
|
|
||||||
} else {
|
|
||||||
initialOptions.inject = [...polyfills];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
import util from 'util';
|
|
||||||
import { extname, join, basename } from 'path';
|
|
||||||
import * as esbuild from 'esbuild';
|
|
||||||
import { promises as fsp } from 'fs';
|
|
||||||
import { IncomingMessage, ServerResponse } from 'http';
|
|
||||||
import libGlob from 'glob';
|
|
||||||
import Proxy from 'http-proxy';
|
|
||||||
import { _experimental_updateFunctionsManifest } from '@vercel/build-utils';
|
|
||||||
|
|
||||||
import { run } from './websandbox';
|
|
||||||
import type { FetchEventResult } from './websandbox/types';
|
|
||||||
|
|
||||||
import { ParsedUrlQuery, stringify as stringifyQs } from 'querystring';
|
|
||||||
import {
|
|
||||||
format as formatUrl,
|
|
||||||
parse as parseUrl,
|
|
||||||
UrlWithParsedQuery,
|
|
||||||
} from 'url';
|
|
||||||
import { toNodeHeaders } from './websandbox/utils';
|
|
||||||
import { nodeProcessPolyfillPlugin } from './esbuild-plugins';
|
|
||||||
|
|
||||||
const glob = util.promisify(libGlob);
|
|
||||||
const SUPPORTED_EXTENSIONS = ['.js', '.ts'];
|
|
||||||
|
|
||||||
// File name of the `entries.js` file that gets copied into the
|
|
||||||
// project directory. Use a name that is unlikely to conflict.
|
|
||||||
const TMP_ENTRIES_NAME = '.output/inputs/middleware/___vc_entries.js';
|
|
||||||
const TMP_MIDDLEWARE_BUNDLE = '.output/inputs/middleware/_temp_middleware.js';
|
|
||||||
|
|
||||||
async function getMiddlewareFile(workingDirectory: string) {
|
|
||||||
// Only the root-level `_middleware.*` files are considered.
|
|
||||||
// For more granular routing, the Project's Framework (i.e. Next.js)
|
|
||||||
// middleware support should be used.
|
|
||||||
const middlewareFiles = await glob(join(workingDirectory, '_middleware.*'));
|
|
||||||
|
|
||||||
if (middlewareFiles.length === 0) {
|
|
||||||
// No middleware file at the root of the project, so bail...
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (middlewareFiles.length > 1) {
|
|
||||||
throw new Error(
|
|
||||||
`Only one middleware file is allowed. Found: ${middlewareFiles.join(
|
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ext = extname(middlewareFiles[0]);
|
|
||||||
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
||||||
throw new Error(`Unsupported file type: ${ext}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return middlewareFiles[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function build({ workPath }: { workPath: string }) {
|
|
||||||
const entriesPath = join(workPath, TMP_ENTRIES_NAME);
|
|
||||||
const transientFilePath = join(workPath, TMP_MIDDLEWARE_BUNDLE);
|
|
||||||
const middlewareFile = await getMiddlewareFile(workPath);
|
|
||||||
if (!middlewareFile) return;
|
|
||||||
|
|
||||||
console.log('Compiling middleware file: %j', middlewareFile);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Two builds happen here, because esbuild doesn't offer a way to add a banner
|
|
||||||
* to individual input files, and the entries wrapper relies on running in
|
|
||||||
* non-strict mode to access the ENTRIES global.
|
|
||||||
*
|
|
||||||
* To work around this, we bundle the middleware directly and add
|
|
||||||
* 'use strict'; to make the entire bundle run in strict mode. We then bundle
|
|
||||||
* a second time, adding the global ENTRIES wrapper and preserving the
|
|
||||||
* 'use strict' for the entire scope of the original bundle.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
await esbuild.build({
|
|
||||||
entryPoints: [middlewareFile],
|
|
||||||
bundle: true,
|
|
||||||
absWorkingDir: workPath,
|
|
||||||
outfile: transientFilePath,
|
|
||||||
banner: {
|
|
||||||
js: '"use strict";',
|
|
||||||
},
|
|
||||||
plugins: [nodeProcessPolyfillPlugin({ env: process.env })],
|
|
||||||
format: 'cjs',
|
|
||||||
});
|
|
||||||
// Create `_ENTRIES` wrapper
|
|
||||||
await fsp.copyFile(join(__dirname, 'entries.js'), entriesPath);
|
|
||||||
await esbuild.build({
|
|
||||||
entryPoints: [entriesPath],
|
|
||||||
bundle: true,
|
|
||||||
absWorkingDir: workPath,
|
|
||||||
outfile: join(workPath, '.output/server/pages/_middleware.js'),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
await fsp.unlink(transientFilePath);
|
|
||||||
await fsp.unlink(entriesPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = basename(middlewareFile);
|
|
||||||
const pages: { [key: string]: any } = {};
|
|
||||||
|
|
||||||
pages[fileName] = {
|
|
||||||
runtime: 'web',
|
|
||||||
env: [],
|
|
||||||
files: ['server/pages/_middleware.js'],
|
|
||||||
name: 'pages/_middleware',
|
|
||||||
page: '/',
|
|
||||||
regexp: '^/.*$',
|
|
||||||
sortingIndex: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
await _experimental_updateFunctionsManifest({ workPath, pages });
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringifyQuery = (req: IncomingMessage, query: ParsedUrlQuery) => {
|
|
||||||
const initialQueryValues = Object.values((req as any).__NEXT_INIT_QUERY);
|
|
||||||
|
|
||||||
return stringifyQs(query, undefined, undefined, {
|
|
||||||
encodeURIComponent(value: any) {
|
|
||||||
if (initialQueryValues.some(val => val === value)) {
|
|
||||||
return encodeURIComponent(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
async function runMiddlewareCatchAll(
|
|
||||||
req: IncomingMessage,
|
|
||||||
res: ServerResponse,
|
|
||||||
requestId: string,
|
|
||||||
name: string,
|
|
||||||
path: string
|
|
||||||
) {
|
|
||||||
let result: FetchEventResult | null = null;
|
|
||||||
const parsedUrl = parseUrl(req.url!, true);
|
|
||||||
try {
|
|
||||||
result = await runMiddleware({
|
|
||||||
request: req,
|
|
||||||
response: res,
|
|
||||||
name: name,
|
|
||||||
path,
|
|
||||||
requestId: requestId,
|
|
||||||
parsedUrl,
|
|
||||||
parsed: parseUrl(req.url!, true),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return { finished: true, error: err };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result === null) {
|
|
||||||
return { finished: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!result.response.headers.has('x-middleware-rewrite') &&
|
|
||||||
!result.response.headers.has('x-middleware-next') &&
|
|
||||||
!result.response.headers.has('Location')
|
|
||||||
) {
|
|
||||||
result.response.headers.set('x-middleware-refresh', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
result.response.headers.delete('x-middleware-next');
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(
|
|
||||||
toNodeHeaders(result.response.headers)
|
|
||||||
)) {
|
|
||||||
if (key !== 'content-encoding' && value !== undefined) {
|
|
||||||
res.setHeader(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preflight =
|
|
||||||
req.method === 'HEAD' && req.headers['x-middleware-preflight'];
|
|
||||||
|
|
||||||
if (preflight) {
|
|
||||||
res.writeHead(200);
|
|
||||||
res.end();
|
|
||||||
return {
|
|
||||||
finished: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
res.statusCode = result.response.status;
|
|
||||||
res.statusMessage = result.response.statusText;
|
|
||||||
|
|
||||||
const location = result.response.headers.get('Location');
|
|
||||||
if (location) {
|
|
||||||
res.statusCode = result.response.status;
|
|
||||||
if (res.statusCode === 308) {
|
|
||||||
res.setHeader('Refresh', `0;url=${location}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return {
|
|
||||||
finished: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.response.headers.has('x-middleware-rewrite')) {
|
|
||||||
const rewrite = result.response.headers.get('x-middleware-rewrite')!;
|
|
||||||
const rewriteParsed = parseUrl(rewrite, true);
|
|
||||||
if (rewriteParsed.protocol) {
|
|
||||||
return proxyRequest(req, res, rewriteParsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
(req as any)._nextRewroteUrl = rewrite;
|
|
||||||
(req as any)._nextDidRewrite = (req as any)._nextRewroteUrl !== req.url;
|
|
||||||
|
|
||||||
return {
|
|
||||||
finished: false,
|
|
||||||
pathname: rewriteParsed.pathname,
|
|
||||||
query: {
|
|
||||||
...parsedUrl.query,
|
|
||||||
...rewriteParsed.query,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.response.headers.has('x-middleware-refresh')) {
|
|
||||||
res.writeHead(result.response.status);
|
|
||||||
|
|
||||||
if (result.response.body instanceof Buffer) {
|
|
||||||
res.write(result.response.body);
|
|
||||||
} else {
|
|
||||||
//@ts-ignore
|
|
||||||
for await (const chunk of result.response.body || []) {
|
|
||||||
res.write(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
return {
|
|
||||||
finished: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
finished: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyRequest = async (
|
|
||||||
req: IncomingMessage,
|
|
||||||
res: ServerResponse,
|
|
||||||
parsedUrl: UrlWithParsedQuery
|
|
||||||
) => {
|
|
||||||
const { query } = parsedUrl;
|
|
||||||
delete (parsedUrl as any).query;
|
|
||||||
parsedUrl.search = stringifyQuery(req, query);
|
|
||||||
|
|
||||||
const target = formatUrl(parsedUrl);
|
|
||||||
const proxy = new Proxy({
|
|
||||||
target,
|
|
||||||
changeOrigin: true,
|
|
||||||
ignorePath: true,
|
|
||||||
xfwd: true,
|
|
||||||
proxyTimeout: 30_000, // limit proxying to 30 seconds
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((proxyResolve, proxyReject) => {
|
|
||||||
let finished = false;
|
|
||||||
|
|
||||||
proxy.on('proxyReq', (proxyReq: any) => {
|
|
||||||
proxyReq.on('close', () => {
|
|
||||||
if (!finished) {
|
|
||||||
finished = true;
|
|
||||||
proxyResolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
proxy.on('error', (err: any) => {
|
|
||||||
if (!finished) {
|
|
||||||
finished = true;
|
|
||||||
proxyReject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
proxy.web(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
finished: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function runMiddleware(params: {
|
|
||||||
request: IncomingMessage;
|
|
||||||
response: ServerResponse;
|
|
||||||
parsedUrl: UrlWithParsedQuery;
|
|
||||||
parsed: UrlWithParsedQuery;
|
|
||||||
requestId: string;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}): Promise<FetchEventResult | null> {
|
|
||||||
const page: { name?: string; params?: { [key: string]: string } } = {};
|
|
||||||
let result: FetchEventResult | null = null;
|
|
||||||
|
|
||||||
result = await run({
|
|
||||||
name: params.name,
|
|
||||||
path: params.path,
|
|
||||||
request: {
|
|
||||||
headers: params.request.headers,
|
|
||||||
method: params.request.method || 'GET',
|
|
||||||
url: params.request.url!,
|
|
||||||
// url: (params.request as any).__NEXT_INIT_URL,
|
|
||||||
page,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
result.waitUntil.catch((error: any) => {
|
|
||||||
console.error(`Uncaught: middleware waitUntil errored`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should run the middleware in the `vm` sandbox and return the result
|
|
||||||
// back to `vercel dev`. If no middleware file exists then this function
|
|
||||||
// should return `finished: false` (very quickly, since this is being
|
|
||||||
// invoked for every HTTP request!).
|
|
||||||
export async function runDevMiddleware(
|
|
||||||
req: IncomingMessage,
|
|
||||||
res: ServerResponse,
|
|
||||||
workingDirectory: string
|
|
||||||
): ReturnType<typeof runMiddlewareCatchAll> {
|
|
||||||
const middlewareFile = await getMiddlewareFile(workingDirectory);
|
|
||||||
if (!middlewareFile) {
|
|
||||||
return {
|
|
||||||
finished: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return runMiddlewareCatchAll(
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
'',
|
|
||||||
basename(middlewareFile),
|
|
||||||
middlewareFile
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import type { RequestData, FetchEventResult } from './types';
|
|
||||||
import { DeprecationError } from './error';
|
|
||||||
import { fromNodeHeaders } from './utils';
|
|
||||||
import { NextFetchEvent } from './spec-extension/fetch-event';
|
|
||||||
import { NextRequest, RequestInit } from './spec-extension/request';
|
|
||||||
import { SpecResponse } from './spec-extension/response';
|
|
||||||
import { waitUntilSymbol } from './spec-compliant/fetch-event';
|
|
||||||
import { Response } from 'node-fetch';
|
|
||||||
|
|
||||||
export async function adapter(params: {
|
|
||||||
handler: (request: NextRequest, event: NextFetchEvent) => Promise<Response>;
|
|
||||||
page: string;
|
|
||||||
request: RequestData;
|
|
||||||
}): Promise<FetchEventResult> {
|
|
||||||
const url = params.request.url.startsWith('/')
|
|
||||||
? `https://${params.request.headers.host}${params.request.url}`
|
|
||||||
: params.request.url;
|
|
||||||
|
|
||||||
const request = new NextRequestHint({
|
|
||||||
page: params.page,
|
|
||||||
input: url,
|
|
||||||
init: {
|
|
||||||
geo: params.request.geo,
|
|
||||||
//@ts-ignore
|
|
||||||
headers: fromNodeHeaders(params.request.headers),
|
|
||||||
ip: params.request.ip,
|
|
||||||
method: params.request.method,
|
|
||||||
page: params.request.page,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = new NextFetchEvent({ request, page: params.page });
|
|
||||||
const original = await params.handler(request, event);
|
|
||||||
|
|
||||||
return {
|
|
||||||
response: original || SpecResponse.next(),
|
|
||||||
waitUntil: Promise.all(event[waitUntilSymbol]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class NextRequestHint extends NextRequest {
|
|
||||||
sourcePage: string;
|
|
||||||
|
|
||||||
constructor(params: {
|
|
||||||
init: RequestInit;
|
|
||||||
input: Request | string;
|
|
||||||
page: string;
|
|
||||||
}) {
|
|
||||||
//@ts-ignore
|
|
||||||
super(params.input, params.init);
|
|
||||||
this.sourcePage = params.page;
|
|
||||||
}
|
|
||||||
|
|
||||||
get request() {
|
|
||||||
throw new DeprecationError({ page: this.sourcePage });
|
|
||||||
}
|
|
||||||
|
|
||||||
respondWith() {
|
|
||||||
throw new DeprecationError({ page: this.sourcePage });
|
|
||||||
}
|
|
||||||
|
|
||||||
waitUntil() {
|
|
||||||
throw new DeprecationError({ page: this.sourcePage });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
export class DeprecationError extends Error {
|
|
||||||
constructor({ page }: { page: string }) {
|
|
||||||
super(`The middleware "${page}" accepts an async API directly with the form:
|
|
||||||
|
|
||||||
export function middleware(request, event) {
|
|
||||||
return new Response("Hello " + request.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
Read more: https://nextjs.org/docs/messages/middleware-new-signature
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { isBlob } from './is';
|
|
||||||
import { streamToIterator } from './utils';
|
|
||||||
|
|
||||||
const carriage = '\r\n';
|
|
||||||
const dashes = '--';
|
|
||||||
const carriageLength = 2;
|
|
||||||
|
|
||||||
function escape(str: string) {
|
|
||||||
return str.replace(/"/g, '\\"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFooter(boundary: string) {
|
|
||||||
return `${dashes}${boundary}${dashes}${carriage.repeat(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHeader(boundary: string, name: string, field: FormDataEntryValue) {
|
|
||||||
let header = '';
|
|
||||||
header += `${dashes}${boundary}${carriage}`;
|
|
||||||
header += `Content-Disposition: form-data; name="${escape(name)}"`;
|
|
||||||
|
|
||||||
if (isBlob(field)) {
|
|
||||||
header += `; filename="${escape(field.name)}"${carriage}`;
|
|
||||||
header += `Content-Type: ${field.type || 'application/octet-stream'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${header}${carriage.repeat(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBoundary() {
|
|
||||||
const array = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(array);
|
|
||||||
|
|
||||||
let str = '';
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
str += array[i].toString(16).padStart(2, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* formDataIterator(
|
|
||||||
form: FormData,
|
|
||||||
boundary: string
|
|
||||||
): AsyncIterableIterator<Uint8Array> {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
//@ts-ignore
|
|
||||||
for (const [name, value] of form) {
|
|
||||||
yield encoder.encode(getHeader(boundary, name, value));
|
|
||||||
|
|
||||||
if (isBlob(value)) {
|
|
||||||
// @ts-ignore /shrug
|
|
||||||
const stream: ReadableStream<Uint8Array> = value.stream();
|
|
||||||
yield* streamToIterator(stream);
|
|
||||||
} else {
|
|
||||||
yield encoder.encode(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield encoder.encode(carriage);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield encoder.encode(getFooter(boundary));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFormDataLength(form: FormData, boundary: string) {
|
|
||||||
let length = 0;
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
for (const [name, value] of form) {
|
|
||||||
length += Buffer.byteLength(getHeader(boundary, name, value));
|
|
||||||
length += isBlob(value) ? value.size : Buffer.byteLength(String(value));
|
|
||||||
length += carriageLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
length += Buffer.byteLength(getFooter(boundary));
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sandbox/sandbox';
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* The ArrayBuffer object is used to represent a generic, fixed-length raw
|
|
||||||
* binary data buffer. It is an array of bytes, often referred to in other
|
|
||||||
* languages as a "byte array". You cannot directly manipulate the contents of
|
|
||||||
* an ArrayBuffer; instead, you create one of the typed array objects or a
|
|
||||||
* DataView object which represents the buffer in a specific format, and use
|
|
||||||
* that to read and write the contents of the buffer.
|
|
||||||
*/
|
|
||||||
export function isArrayBuffer(value: any): value is ArrayBuffer {
|
|
||||||
return Object.prototype.isPrototypeOf.call(ArrayBuffer, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ArrayBufferView is a helper type representing any of the following JS
|
|
||||||
* TypedArray types which correspond to the list below. It is checked by duck
|
|
||||||
* typing the provided object.
|
|
||||||
*/
|
|
||||||
export function isArrayBufferView(value: any): value is ArrayBufferView {
|
|
||||||
return ArrayBuffer.isView(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The DataView view provides a low-level interface for reading and writing
|
|
||||||
* multiple number types in a binary ArrayBuffer, without having to care about
|
|
||||||
* the platform's endianness.
|
|
||||||
*/
|
|
||||||
export function isDataView(value: any): value is DataView {
|
|
||||||
return Object.prototype.isPrototypeOf.call(DataView, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URLSearchParams interface defines utility methods to work with the
|
|
||||||
* query string of a URL.
|
|
||||||
*/
|
|
||||||
export function isURLSearchParams(value: any): value is URLSearchParams {
|
|
||||||
return Object.prototype.isPrototypeOf.call(URLSearchParams, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Blob object represents a blob, which is a file-like object of immutable,
|
|
||||||
* raw data; they can be read as text or binary data. Blobs can represent data
|
|
||||||
* that isn't necessarily in a JavaScript-native format.
|
|
||||||
*/
|
|
||||||
export function isBlob(value: any): value is Blob {
|
|
||||||
return Object.prototype.isPrototypeOf.call(Blob, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The FormData interface provides a way to easily construct a set of key/value
|
|
||||||
* pairs representing form fields and their values, which can then be easily
|
|
||||||
* sent using the XMLHttpRequest.send() method. It uses the same format a
|
|
||||||
* form would use if the encoding type were set to "multipart/form-data".
|
|
||||||
*/
|
|
||||||
export function isFormData(value: any): value is FormData {
|
|
||||||
return Object.prototype.isPrototypeOf.call(FormData, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ReadableStream interface of the Streams API represents a readable stream
|
|
||||||
* of byte data. Because we want to allow alternative implementations we also
|
|
||||||
* duck type here.
|
|
||||||
*/
|
|
||||||
export function isReadableStream(value: any): value is ReadableStream {
|
|
||||||
return (
|
|
||||||
value &&
|
|
||||||
(Object.prototype.isPrototypeOf.call(ReadableStream, value) ||
|
|
||||||
(value.constructor.name === 'ReadableStream' && 'getReader' in value))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks in an object implements an Iterable interface
|
|
||||||
*/
|
|
||||||
export function isIterable(object: any): object is Iterable<unknown> {
|
|
||||||
return (
|
|
||||||
object &&
|
|
||||||
Symbol.iterator in object &&
|
|
||||||
typeof object[Symbol.iterator] === 'function'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Crypto as WebCrypto } from '@peculiar/webcrypto';
|
|
||||||
import { TransformStream } from 'web-streams-polyfill';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
export function atob(b64Encoded: string) {
|
|
||||||
return Buffer.from(b64Encoded, 'base64').toString('binary');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function btoa(str: string) {
|
|
||||||
return Buffer.from(str, 'binary').toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextEncoderRuntime {
|
|
||||||
encoder: TextEncoder;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.encoder = new TextEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
get encoding() {
|
|
||||||
return this.encoder.encoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public encode(input: string) {
|
|
||||||
return this.encoder.encode(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextDecoderRuntime {
|
|
||||||
decoder: TextDecoder;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.decoder = new TextDecoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
get encoding() {
|
|
||||||
return this.decoder.encoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fatal() {
|
|
||||||
return this.decoder.fatal;
|
|
||||||
}
|
|
||||||
|
|
||||||
get ignoreBOM() {
|
|
||||||
return this.decoder.ignoreBOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
public decode(input: BufferSource, options?: TextDecodeOptions) {
|
|
||||||
return this.decoder.decode(input, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { TextDecoderRuntime as TextDecoder };
|
|
||||||
export { TextEncoderRuntime as TextEncoder };
|
|
||||||
|
|
||||||
export class Crypto extends WebCrypto {
|
|
||||||
// @ts-ignore Remove once types are updated and we deprecate node 12
|
|
||||||
randomUUID = crypto.randomUUID || uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReadableStream<T> {
|
|
||||||
constructor(opts: UnderlyingSource = {}) {
|
|
||||||
let closed = false;
|
|
||||||
let pullPromise: any;
|
|
||||||
|
|
||||||
let transformController: TransformStreamDefaultController;
|
|
||||||
const { readable, writable } = new TransformStream(
|
|
||||||
{
|
|
||||||
start: (controller: TransformStreamDefaultController) => {
|
|
||||||
transformController = controller;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
highWaterMark: 1,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const writer = writable.getWriter();
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const controller: ReadableStreamController<T> = {
|
|
||||||
get desiredSize() {
|
|
||||||
return transformController.desiredSize;
|
|
||||||
},
|
|
||||||
close: () => {
|
|
||||||
if (!closed) {
|
|
||||||
closed = true;
|
|
||||||
writer.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enqueue: (chunk: T) => {
|
|
||||||
writer.write(typeof chunk === 'string' ? encoder.encode(chunk) : chunk);
|
|
||||||
pull();
|
|
||||||
},
|
|
||||||
error: (reason: any) => {
|
|
||||||
transformController.error(reason);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const pull = () => {
|
|
||||||
if (opts.pull) {
|
|
||||||
if (!pullPromise) {
|
|
||||||
pullPromise = Promise.resolve().then(() => {
|
|
||||||
pullPromise = 0;
|
|
||||||
opts.pull!(controller);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (opts.start) {
|
|
||||||
opts.start(controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.cancel) {
|
|
||||||
readable.cancel = (reason: any) => {
|
|
||||||
opts.cancel!(reason);
|
|
||||||
return readable.cancel(reason);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pull();
|
|
||||||
|
|
||||||
return readable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import type { RequestData, FetchEventResult, NodeHeaders } from '../types';
|
|
||||||
import { Blob, File, FormData } from 'formdata-node';
|
|
||||||
import { dirname, extname, resolve } from 'path';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { TransformStream } from 'web-streams-polyfill';
|
|
||||||
import * as polyfills from './polyfills';
|
|
||||||
import cookie from 'cookie';
|
|
||||||
import vm from 'vm';
|
|
||||||
import fetch, {
|
|
||||||
Headers,
|
|
||||||
RequestInit,
|
|
||||||
Response,
|
|
||||||
Request,
|
|
||||||
RequestInfo,
|
|
||||||
} from 'node-fetch';
|
|
||||||
import { adapter } from '../adapter';
|
|
||||||
import * as esbuild from 'esbuild';
|
|
||||||
import m from 'module';
|
|
||||||
|
|
||||||
interface URLLike {
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache:
|
|
||||||
| {
|
|
||||||
context: { [key: string]: any };
|
|
||||||
paths: Map<string, string>;
|
|
||||||
require: Map<string, any>;
|
|
||||||
sandbox: vm.Context;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const WEBPACK_HASH_REGEX =
|
|
||||||
/__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cache is cleared when a path is cached and the content has changed. The
|
|
||||||
* hack ignores changes than only change the compilation hash. Instead it is
|
|
||||||
* probably better to disable HMR for middleware entries.
|
|
||||||
*/
|
|
||||||
export function clearSandboxCache(path: string, content: Buffer | string) {
|
|
||||||
const prev = cache?.paths.get(path)?.replace(WEBPACK_HASH_REGEX, '');
|
|
||||||
if (prev === undefined) return;
|
|
||||||
if (prev === content.toString().replace(WEBPACK_HASH_REGEX, '')) return;
|
|
||||||
cache = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function run(params: {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
request: RequestData;
|
|
||||||
}): Promise<FetchEventResult> {
|
|
||||||
if (cache === undefined) {
|
|
||||||
const context: { [key: string]: any } = {
|
|
||||||
atob: polyfills.atob,
|
|
||||||
Blob,
|
|
||||||
btoa: polyfills.btoa,
|
|
||||||
clearInterval,
|
|
||||||
clearTimeout,
|
|
||||||
console: {
|
|
||||||
assert: console.assert.bind(console),
|
|
||||||
error: console.error.bind(console),
|
|
||||||
info: console.info.bind(console),
|
|
||||||
log: console.log.bind(console),
|
|
||||||
time: console.time.bind(console),
|
|
||||||
timeEnd: console.timeEnd.bind(console),
|
|
||||||
timeLog: console.timeLog.bind(console),
|
|
||||||
warn: console.warn.bind(console),
|
|
||||||
},
|
|
||||||
Crypto: polyfills.Crypto,
|
|
||||||
crypto: new polyfills.Crypto(),
|
|
||||||
Response,
|
|
||||||
Headers,
|
|
||||||
Request,
|
|
||||||
fetch: (input: RequestInfo, init: RequestInit = {}) => {
|
|
||||||
const url = getFetchURL(input, params.request.headers);
|
|
||||||
init.headers = getFetchHeaders(params.name, init);
|
|
||||||
if (isRequestLike(input)) {
|
|
||||||
return fetch(url, {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...Object.fromEntries(input.headers),
|
|
||||||
...Object.fromEntries(init.headers),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return fetch(url, init);
|
|
||||||
},
|
|
||||||
File,
|
|
||||||
FormData,
|
|
||||||
process: { env: { ...process.env } },
|
|
||||||
ReadableStream: polyfills.ReadableStream,
|
|
||||||
setInterval,
|
|
||||||
setTimeout,
|
|
||||||
TextDecoder: polyfills.TextDecoder,
|
|
||||||
TextEncoder: polyfills.TextEncoder,
|
|
||||||
TransformStream,
|
|
||||||
URL,
|
|
||||||
URLSearchParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
context.self = context;
|
|
||||||
|
|
||||||
cache = {
|
|
||||||
context,
|
|
||||||
require: new Map<string, any>([
|
|
||||||
[require.resolve('cookie'), { exports: cookie }],
|
|
||||||
]),
|
|
||||||
paths: new Map<string, string>(),
|
|
||||||
sandbox: vm.createContext(context),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const content = readFileSync(params.path, 'utf-8');
|
|
||||||
const esBuildResult = esbuild.transformSync(content, {
|
|
||||||
format: 'cjs',
|
|
||||||
banner: '"use strict";',
|
|
||||||
});
|
|
||||||
const x = vm.runInNewContext(m.wrap(esBuildResult.code), cache.sandbox, {
|
|
||||||
filename: params.path,
|
|
||||||
});
|
|
||||||
const module = {
|
|
||||||
exports: {},
|
|
||||||
loaded: false,
|
|
||||||
id: params.path,
|
|
||||||
};
|
|
||||||
x(
|
|
||||||
module.exports,
|
|
||||||
sandboxRequire.bind(null, params.path),
|
|
||||||
module,
|
|
||||||
dirname(params.path),
|
|
||||||
params.path
|
|
||||||
);
|
|
||||||
const adapterResult = await adapter({
|
|
||||||
request: params.request,
|
|
||||||
// @ts-ignore
|
|
||||||
handler: module.exports.default,
|
|
||||||
page: params.path,
|
|
||||||
});
|
|
||||||
return adapterResult;
|
|
||||||
} catch (error) {
|
|
||||||
cache = undefined;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sandboxRequire(referrer: string, specifier: string) {
|
|
||||||
const resolved = require.resolve(specifier, {
|
|
||||||
paths: [resolve(dirname(referrer))],
|
|
||||||
});
|
|
||||||
|
|
||||||
const cached = cache?.require.get(resolved);
|
|
||||||
if (cached !== undefined) {
|
|
||||||
return cached.exports;
|
|
||||||
}
|
|
||||||
|
|
||||||
const module = {
|
|
||||||
exports: {},
|
|
||||||
loaded: false,
|
|
||||||
id: resolved,
|
|
||||||
};
|
|
||||||
|
|
||||||
cache?.require.set(resolved, module);
|
|
||||||
|
|
||||||
const transformOptions: esbuild.TransformOptions = {
|
|
||||||
format: 'cjs',
|
|
||||||
banner: '"use strict";',
|
|
||||||
};
|
|
||||||
if (extname(resolved) === '.json') {
|
|
||||||
transformOptions.loader = 'json';
|
|
||||||
}
|
|
||||||
const transformedContent = esbuild.transformSync(
|
|
||||||
readFileSync(resolved, 'utf-8'),
|
|
||||||
transformOptions
|
|
||||||
).code;
|
|
||||||
const fn = vm.runInContext(
|
|
||||||
`(function(module,exports,require,__dirname,__filename) {${transformedContent}\n})`,
|
|
||||||
cache!.sandbox
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
fn(
|
|
||||||
module,
|
|
||||||
module.exports,
|
|
||||||
sandboxRequire.bind(null, resolved),
|
|
||||||
dirname(resolved),
|
|
||||||
resolved
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
cache?.require.delete(resolved);
|
|
||||||
}
|
|
||||||
module.loaded = true;
|
|
||||||
return module.exports;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFetchHeaders(middleware: string, init: RequestInit) {
|
|
||||||
const headers = new Headers(init.headers ?? {});
|
|
||||||
const prevsub = headers.get(`x-middleware-subrequest`) || '';
|
|
||||||
const value = prevsub.split(':').concat(middleware).join(':');
|
|
||||||
headers.set(`x-middleware-subrequest`, value);
|
|
||||||
headers.set(`user-agent`, `Next.js Middleware`);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string {
|
|
||||||
const initurl = isRequestLike(input)
|
|
||||||
? input.url
|
|
||||||
: isURLLike(input)
|
|
||||||
? input.href
|
|
||||||
: input;
|
|
||||||
if (initurl.startsWith('/')) {
|
|
||||||
const host = headers.host?.toString();
|
|
||||||
const localhost =
|
|
||||||
host === '127.0.0.1' ||
|
|
||||||
host === 'localhost' ||
|
|
||||||
host?.startsWith('localhost:');
|
|
||||||
return `${localhost ? 'http' : 'https'}://${host}${initurl}`;
|
|
||||||
}
|
|
||||||
return initurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isURLLike(obj: unknown): obj is URLLike {
|
|
||||||
return Boolean(obj && typeof obj === 'object' && 'href' in obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRequestLike(obj: unknown): obj is Request {
|
|
||||||
return Boolean(obj && typeof obj === 'object' && 'url' in obj);
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import { formDataIterator, getBoundary } from '../form-data';
|
|
||||||
import { streamToIterator } from '../utils';
|
|
||||||
import * as util from '../is';
|
|
||||||
import { URLSearchParams } from 'url';
|
|
||||||
|
|
||||||
const INTERNALS = Symbol('internal body');
|
|
||||||
|
|
||||||
abstract class BaseBody implements Body {
|
|
||||||
abstract headers: Headers;
|
|
||||||
|
|
||||||
[INTERNALS]: {
|
|
||||||
bodyInit?: BodyInit;
|
|
||||||
boundary?: string;
|
|
||||||
disturbed: boolean;
|
|
||||||
stream?: ReadableStream<Uint8Array> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(bodyInit?: BodyInit) {
|
|
||||||
this[INTERNALS] = {
|
|
||||||
bodyInit: bodyInit,
|
|
||||||
disturbed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (util.isFormData(bodyInit)) {
|
|
||||||
this[INTERNALS].boundary = getBoundary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get body(): ReadableStream<Uint8Array> | null {
|
|
||||||
const body = this[INTERNALS].bodyInit;
|
|
||||||
if (!body) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const that = this;
|
|
||||||
if (!this[INTERNALS].stream) {
|
|
||||||
const readable = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
controller.enqueue(encoder.encode(body));
|
|
||||||
} else if (util.isBlob(body)) {
|
|
||||||
const buffer = await body.arrayBuffer();
|
|
||||||
controller.enqueue(new Uint8Array(buffer));
|
|
||||||
} else if (util.isDataView(body)) {
|
|
||||||
controller.enqueue(body);
|
|
||||||
} else if (util.isArrayBuffer(body)) {
|
|
||||||
controller.enqueue(body);
|
|
||||||
} else if (util.isArrayBufferView(body)) {
|
|
||||||
controller.enqueue(body);
|
|
||||||
} else if (util.isURLSearchParams(body)) {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
controller.enqueue(encoder.encode(body.toString()));
|
|
||||||
} else if (util.isFormData(body)) {
|
|
||||||
for await (const chunk of formDataIterator(
|
|
||||||
body,
|
|
||||||
that[INTERNALS].boundary!
|
|
||||||
)) {
|
|
||||||
controller.enqueue(chunk);
|
|
||||||
}
|
|
||||||
} else if (util.isReadableStream(body)) {
|
|
||||||
for await (const chunk of streamToIterator(body)) {
|
|
||||||
if (chunk.length) {
|
|
||||||
controller.enqueue(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const text = Object.prototype.toString.call(body);
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
controller.enqueue(encoder.encode(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spy on reading chunks to set the stream as disturbed
|
|
||||||
const getReader = readable.getReader.bind(readable);
|
|
||||||
readable.getReader = () => {
|
|
||||||
const reader = getReader();
|
|
||||||
const read = reader.read.bind(reader);
|
|
||||||
reader.read = () => {
|
|
||||||
this[INTERNALS].disturbed = true;
|
|
||||||
return read();
|
|
||||||
};
|
|
||||||
return reader;
|
|
||||||
};
|
|
||||||
|
|
||||||
this[INTERNALS].stream = readable;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this[INTERNALS].stream!;
|
|
||||||
}
|
|
||||||
|
|
||||||
get bodyUsed(): boolean {
|
|
||||||
return this[INTERNALS].disturbed;
|
|
||||||
}
|
|
||||||
|
|
||||||
_consume() {
|
|
||||||
if (this[INTERNALS].disturbed) {
|
|
||||||
return Promise.reject(
|
|
||||||
new TypeError(
|
|
||||||
`Body has already been used. It can only be used once. Use tee() first if you need to read it twice.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this[INTERNALS].disturbed = true;
|
|
||||||
const body = this.body;
|
|
||||||
return new Promise<Uint8Array>((resolve, reject) => {
|
|
||||||
let buffer = new Uint8Array(0);
|
|
||||||
if (!body) {
|
|
||||||
return resolve(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = body.getReader();
|
|
||||||
(function pump() {
|
|
||||||
reader.read().then(({ value, done }) => {
|
|
||||||
if (done) {
|
|
||||||
return resolve(buffer);
|
|
||||||
} else if (value) {
|
|
||||||
const merge = new Uint8Array(buffer.length + value.length);
|
|
||||||
merge.set(buffer);
|
|
||||||
merge.set(value, buffer.length);
|
|
||||||
buffer = merge;
|
|
||||||
}
|
|
||||||
|
|
||||||
pump();
|
|
||||||
}, reject);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async arrayBuffer() {
|
|
||||||
const buffer = await this._consume();
|
|
||||||
const arrayBuffer = new ArrayBuffer(buffer.length);
|
|
||||||
const view = new Uint8Array(arrayBuffer);
|
|
||||||
|
|
||||||
for (let i = 0; i < buffer.length; ++i) {
|
|
||||||
view[i] = buffer[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return arrayBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async blob() {
|
|
||||||
const buffer = await this._consume();
|
|
||||||
return new Blob([buffer]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async formData() {
|
|
||||||
const bodyInit = this[INTERNALS].bodyInit;
|
|
||||||
if (util.isURLSearchParams(bodyInit)) {
|
|
||||||
const form = new FormData();
|
|
||||||
for (const [key, value] of bodyInit) {
|
|
||||||
form.append(key, value);
|
|
||||||
}
|
|
||||||
return form;
|
|
||||||
} else if (util.isFormData(bodyInit)) {
|
|
||||||
return bodyInit;
|
|
||||||
} else {
|
|
||||||
throw new TypeError(
|
|
||||||
`Unrecognized Content-Type header value. FormData can only parse the following MIME types: multipart/form-data, application/x-www-form-urlencoded.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async text() {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const buffer = await this._consume();
|
|
||||||
return decoder.decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
async json() {
|
|
||||||
const text = await this.text();
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (err: any) {
|
|
||||||
throw new TypeError(`invalid json body reason: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BaseBody as Body };
|
|
||||||
|
|
||||||
export type BodyInit =
|
|
||||||
| null
|
|
||||||
| string
|
|
||||||
| Blob
|
|
||||||
| BufferSource
|
|
||||||
| FormData
|
|
||||||
| URLSearchParams
|
|
||||||
| ReadableStream<Uint8Array>;
|
|
||||||
|
|
||||||
export function extractContentType(instance: BaseBody) {
|
|
||||||
const body = instance[INTERNALS].bodyInit;
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
return 'text/plain;charset=UTF-8';
|
|
||||||
} else if (util.isBlob(body)) {
|
|
||||||
return body.type;
|
|
||||||
} else if (util.isDataView(body)) {
|
|
||||||
return null;
|
|
||||||
} else if (util.isArrayBuffer(body)) {
|
|
||||||
return null;
|
|
||||||
} else if (util.isArrayBufferView(body)) {
|
|
||||||
return null;
|
|
||||||
} else if (util.isURLSearchParams(body)) {
|
|
||||||
return 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
||||||
} else if (util.isFormData(body)) {
|
|
||||||
return `multipart/form-data;boundary=${instance[INTERNALS].boundary}`;
|
|
||||||
} else if (util.isReadableStream(body)) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return 'text/plain;charset=UTF-8';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloneBody(instance: BaseBody) {
|
|
||||||
if (instance.bodyUsed) {
|
|
||||||
throw new Error('cannot clone body after it is used');
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = instance[INTERNALS].bodyInit;
|
|
||||||
if (util.isReadableStream(body)) {
|
|
||||||
const [r1, r2] = body.tee();
|
|
||||||
instance[INTERNALS].bodyInit = r1;
|
|
||||||
return r2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInstanceBody(instance: BaseBody) {
|
|
||||||
return instance[INTERNALS].bodyInit;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export const responseSymbol = Symbol('response');
|
|
||||||
export const passThroughSymbol = Symbol('passThrough');
|
|
||||||
export const waitUntilSymbol = Symbol('waitUntil');
|
|
||||||
|
|
||||||
export class FetchEvent {
|
|
||||||
readonly [waitUntilSymbol]: Promise<any>[] = [];
|
|
||||||
[responseSymbol]?: Promise<Response>;
|
|
||||||
[passThroughSymbol] = false;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
respondWith(response: Response | Promise<Response>): void {
|
|
||||||
if (!this[responseSymbol]) {
|
|
||||||
this[responseSymbol] = Promise.resolve(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passThroughOnException(): void {
|
|
||||||
this[passThroughSymbol] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
waitUntil(promise: Promise<any>): void {
|
|
||||||
this[waitUntilSymbol].push(promise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
import { isIterable } from '../is';
|
|
||||||
|
|
||||||
const MAP = Symbol('map');
|
|
||||||
const INTERNAL = Symbol('internal');
|
|
||||||
const INVALID_TOKEN_REGEX = /[^^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
|
|
||||||
const INVALID_HEADER_CHAR_REGEX = /[^\t\x20-\x7e\x80-\xff]/;
|
|
||||||
|
|
||||||
class BaseHeaders implements Headers {
|
|
||||||
[MAP]: { [k: string]: string[] } = {};
|
|
||||||
|
|
||||||
constructor(init?: HeadersInit) {
|
|
||||||
if (init instanceof BaseHeaders) {
|
|
||||||
const rawHeaders = init.raw();
|
|
||||||
for (const headerName of Object.keys(rawHeaders)) {
|
|
||||||
for (const value of rawHeaders[headerName]) {
|
|
||||||
this.append(headerName, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isIterable(init)) {
|
|
||||||
const pairs = [];
|
|
||||||
for (const pair of init) {
|
|
||||||
if (!isIterable(pair)) {
|
|
||||||
throw new TypeError('Each header pair must be iterable');
|
|
||||||
}
|
|
||||||
pairs.push(Array.from(pair));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pair of pairs) {
|
|
||||||
if (pair.length !== 2) {
|
|
||||||
throw new TypeError('Each header pair must be a name/value tuple');
|
|
||||||
}
|
|
||||||
this.append(pair[0], pair[1]);
|
|
||||||
}
|
|
||||||
} else if (typeof init === 'object') {
|
|
||||||
for (const key of Object.keys(init)) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.append(key, init[key]);
|
|
||||||
}
|
|
||||||
} else if (init) {
|
|
||||||
throw new TypeError('Provided initializer must be an object');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get(name: string) {
|
|
||||||
const _name = `${name}`;
|
|
||||||
validateName(_name);
|
|
||||||
const key = find(this[MAP], _name);
|
|
||||||
if (key === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this[MAP][key].join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(
|
|
||||||
callback: (value: string, name: string, parent: BaseHeaders) => void,
|
|
||||||
thisArg: any = undefined
|
|
||||||
): void {
|
|
||||||
let pairs = getHeaders(this);
|
|
||||||
let i = 0;
|
|
||||||
while (i < pairs.length) {
|
|
||||||
const [name, value] = pairs[i];
|
|
||||||
callback.call(thisArg, value, name, this);
|
|
||||||
pairs = getHeaders(this);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set(name: string, value: string) {
|
|
||||||
name = `${name}`;
|
|
||||||
value = `${value}`;
|
|
||||||
validateName(name);
|
|
||||||
validateValue(value);
|
|
||||||
const key = find(this[MAP], name);
|
|
||||||
this[MAP][key !== undefined ? key : name] = [value];
|
|
||||||
}
|
|
||||||
|
|
||||||
append(name: string, value: string) {
|
|
||||||
name = `${name}`;
|
|
||||||
value = `${value}`;
|
|
||||||
validateName(name);
|
|
||||||
validateValue(value);
|
|
||||||
const key = find(this[MAP], name);
|
|
||||||
if (key !== undefined) {
|
|
||||||
this[MAP][key].push(value);
|
|
||||||
} else {
|
|
||||||
this[MAP][name] = [value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has(name: string) {
|
|
||||||
name = `${name}`;
|
|
||||||
validateName(name);
|
|
||||||
return find(this[MAP], name) !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(name: string) {
|
|
||||||
name = `${name}`;
|
|
||||||
validateName(name);
|
|
||||||
const key = find(this[MAP], name);
|
|
||||||
if (key !== undefined) {
|
|
||||||
delete this[MAP][key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
raw() {
|
|
||||||
return this[MAP];
|
|
||||||
}
|
|
||||||
|
|
||||||
keys() {
|
|
||||||
return createHeadersIterator(this, 'key');
|
|
||||||
}
|
|
||||||
|
|
||||||
values() {
|
|
||||||
return createHeadersIterator(this, 'value');
|
|
||||||
}
|
|
||||||
|
|
||||||
entries() {
|
|
||||||
return createHeadersIterator(this, 'key+value');
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
return createHeadersIterator(this, 'key+value');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHeadersIterator(
|
|
||||||
target: BaseHeaders,
|
|
||||||
kind: 'key' | 'value' | 'key+value'
|
|
||||||
) {
|
|
||||||
const iterator = Object.create(HeadersIteratorPrototype);
|
|
||||||
iterator[INTERNAL] = {
|
|
||||||
target,
|
|
||||||
kind,
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
return iterator;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateName(name: string) {
|
|
||||||
name = `${name}`;
|
|
||||||
if (INVALID_TOKEN_REGEX.test(name)) {
|
|
||||||
throw new TypeError(`${name} is not a legal HTTP header name`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateValue(value: string) {
|
|
||||||
value = `${value}`;
|
|
||||||
if (INVALID_HEADER_CHAR_REGEX.test(value)) {
|
|
||||||
throw new TypeError(`${value} is not a legal HTTP header value`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function find(
|
|
||||||
map: { [k: string]: string[] },
|
|
||||||
name: string
|
|
||||||
): string | undefined {
|
|
||||||
name = name.toLowerCase();
|
|
||||||
for (const key in map) {
|
|
||||||
if (key.toLowerCase() === name) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.defineProperty(BaseHeaders.prototype, Symbol.toStringTag, {
|
|
||||||
value: 'Headers',
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperties(BaseHeaders.prototype, {
|
|
||||||
append: { enumerable: true },
|
|
||||||
delete: { enumerable: true },
|
|
||||||
entries: { enumerable: true },
|
|
||||||
forEach: { enumerable: true },
|
|
||||||
get: { enumerable: true },
|
|
||||||
has: { enumerable: true },
|
|
||||||
keys: { enumerable: true },
|
|
||||||
raw: { enumerable: false },
|
|
||||||
set: { enumerable: true },
|
|
||||||
values: { enumerable: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
function getHeaders(
|
|
||||||
headers: BaseHeaders,
|
|
||||||
kind: 'key' | 'value' | 'key+value' = 'key+value'
|
|
||||||
) {
|
|
||||||
const fn =
|
|
||||||
kind === 'key'
|
|
||||||
? (key: string) => key.toLowerCase()
|
|
||||||
: kind === 'value'
|
|
||||||
? (key: string) => headers[MAP][key].join(', ')
|
|
||||||
: (key: string) => [key.toLowerCase(), headers[MAP][key].join(', ')];
|
|
||||||
|
|
||||||
return Object.keys(headers[MAP])
|
|
||||||
.sort()
|
|
||||||
.map(key => fn(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
const HeadersIteratorPrototype = Object.setPrototypeOf(
|
|
||||||
{
|
|
||||||
next() {
|
|
||||||
if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
|
|
||||||
throw new TypeError('Value of `this` is not a HeadersIterator');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { target, kind, index } = this[INTERNAL];
|
|
||||||
const values = getHeaders(target, kind);
|
|
||||||
const len = values.length;
|
|
||||||
if (index >= len) {
|
|
||||||
return {
|
|
||||||
value: undefined,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this[INTERNAL].index = index + 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: values[index],
|
|
||||||
done: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, {
|
|
||||||
value: 'HeadersIterator',
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { BaseHeaders as Headers };
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { Body, cloneBody, extractContentType, getInstanceBody } from './body';
|
|
||||||
import { Headers as BaseHeaders } from './headers';
|
|
||||||
import { notImplemented } from '../utils';
|
|
||||||
|
|
||||||
export const INTERNALS = Symbol('internal request');
|
|
||||||
|
|
||||||
class BaseRequest extends Body implements Request {
|
|
||||||
[INTERNALS]: {
|
|
||||||
credentials: RequestCredentials;
|
|
||||||
headers: Headers;
|
|
||||||
method: string;
|
|
||||||
redirect: RequestRedirect;
|
|
||||||
url: URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(input: BaseRequest | string, init: RequestInit = {}) {
|
|
||||||
const method = init.method?.toUpperCase() ?? 'GET';
|
|
||||||
|
|
||||||
if (
|
|
||||||
(method === 'GET' || method === 'HEAD') &&
|
|
||||||
(init.body || (input instanceof BaseRequest && getInstanceBody(input)))
|
|
||||||
) {
|
|
||||||
throw new TypeError('Request with GET/HEAD method cannot have body');
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputBody: BodyInit | null = null;
|
|
||||||
if (init.body) {
|
|
||||||
inputBody = init.body;
|
|
||||||
} else if (input instanceof BaseRequest && getInstanceBody(input)) {
|
|
||||||
inputBody = cloneBody(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
super(inputBody);
|
|
||||||
|
|
||||||
const headers = new BaseHeaders(
|
|
||||||
init.headers || getProp(input, 'headers') || {}
|
|
||||||
);
|
|
||||||
if (inputBody !== null) {
|
|
||||||
const contentType = extractContentType(this);
|
|
||||||
if (contentType !== null && !headers.has('Content-Type')) {
|
|
||||||
headers.append('Content-Type', contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this[INTERNALS] = {
|
|
||||||
credentials:
|
|
||||||
init.credentials || getProp(input, 'credentials') || 'same-origin',
|
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
redirect: init.redirect || getProp(input, 'redirect') || 'follow',
|
|
||||||
url: new URL(typeof input === 'string' ? input : input.url),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get url() {
|
|
||||||
return this[INTERNALS].url.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
get credentials() {
|
|
||||||
return this[INTERNALS].credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
get method() {
|
|
||||||
return this[INTERNALS].method;
|
|
||||||
}
|
|
||||||
|
|
||||||
get headers() {
|
|
||||||
return this[INTERNALS].headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
get redirect() {
|
|
||||||
return this[INTERNALS].redirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
public clone() {
|
|
||||||
return new BaseRequest(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get cache() {
|
|
||||||
return notImplemented('Request', 'cache');
|
|
||||||
}
|
|
||||||
|
|
||||||
get integrity() {
|
|
||||||
return notImplemented('Request', 'integrity');
|
|
||||||
}
|
|
||||||
|
|
||||||
get keepalive() {
|
|
||||||
return notImplemented('Request', 'keepalive');
|
|
||||||
}
|
|
||||||
|
|
||||||
get mode() {
|
|
||||||
return notImplemented('Request', 'mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
get destination() {
|
|
||||||
return notImplemented('Request', 'destination');
|
|
||||||
}
|
|
||||||
|
|
||||||
get referrer() {
|
|
||||||
return notImplemented('Request', 'referrer');
|
|
||||||
}
|
|
||||||
|
|
||||||
get referrerPolicy() {
|
|
||||||
return notImplemented('Request', 'referrerPolicy');
|
|
||||||
}
|
|
||||||
|
|
||||||
get signal() {
|
|
||||||
return notImplemented('Request', 'signal');
|
|
||||||
}
|
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
|
||||||
return 'Request';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BaseRequest as Request };
|
|
||||||
|
|
||||||
function getProp<K extends keyof BaseRequest>(
|
|
||||||
input: BaseRequest | string,
|
|
||||||
key: K
|
|
||||||
): BaseRequest[K] | undefined {
|
|
||||||
return input instanceof BaseRequest ? input[key] : undefined;
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { Body, BodyInit, cloneBody, extractContentType } from './body';
|
|
||||||
|
|
||||||
const INTERNALS = Symbol('internal response');
|
|
||||||
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
||||||
|
|
||||||
class BaseResponse extends Body implements Response {
|
|
||||||
[INTERNALS]: {
|
|
||||||
headers: Headers;
|
|
||||||
status: number;
|
|
||||||
statusText: string;
|
|
||||||
type: 'default' | 'error';
|
|
||||||
url?: URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(body?: BodyInit | null, init?: ResponseInit) {
|
|
||||||
super(body);
|
|
||||||
|
|
||||||
this[INTERNALS] = {
|
|
||||||
headers: new Headers(init?.headers),
|
|
||||||
status: init?.status || 200,
|
|
||||||
statusText: init?.statusText || '',
|
|
||||||
type: 'default',
|
|
||||||
url: init?.url ? new URL(init.url) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this[INTERNALS].status < 200 || this[INTERNALS].status > 599) {
|
|
||||||
throw new RangeError(
|
|
||||||
`Responses may only be constructed with status codes in the range 200 to 599, inclusive.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body !== null && !this[INTERNALS].headers.has('Content-Type')) {
|
|
||||||
const contentType = extractContentType(this);
|
|
||||||
if (contentType) {
|
|
||||||
this[INTERNALS].headers.append('Content-Type', contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static redirect(url: string, status = 302) {
|
|
||||||
if (!REDIRECTS.has(status)) {
|
|
||||||
throw new RangeError(
|
|
||||||
'Failed to execute "redirect" on "response": Invalid status code'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(null, {
|
|
||||||
headers: { Location: url },
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static error() {
|
|
||||||
const response = new BaseResponse(null, { status: 0, statusText: '' });
|
|
||||||
response[INTERNALS].type = 'error';
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
get url() {
|
|
||||||
return this[INTERNALS].url?.toString() || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
get ok() {
|
|
||||||
return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this[INTERNALS].status;
|
|
||||||
}
|
|
||||||
|
|
||||||
get statusText() {
|
|
||||||
return this[INTERNALS].statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
get headers() {
|
|
||||||
return this[INTERNALS].headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
get redirected() {
|
|
||||||
return (
|
|
||||||
this[INTERNALS].status > 299 &&
|
|
||||||
this[INTERNALS].status < 400 &&
|
|
||||||
this[INTERNALS].headers.has('Location')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return this[INTERNALS].type;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
clone() {
|
|
||||||
return new BaseResponse(cloneBody(this), {
|
|
||||||
headers: this.headers,
|
|
||||||
status: this.status,
|
|
||||||
statusText: this.statusText,
|
|
||||||
url: this.url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
|
||||||
return 'Response';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResponseInit {
|
|
||||||
headers?: HeadersInit;
|
|
||||||
status?: number;
|
|
||||||
statusText?: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BaseResponse as Response };
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { DeprecationError } from '../error';
|
|
||||||
import { FetchEvent } from '../spec-compliant/fetch-event';
|
|
||||||
import { NextRequest } from './request';
|
|
||||||
|
|
||||||
export class NextFetchEvent extends FetchEvent {
|
|
||||||
sourcePage: string;
|
|
||||||
|
|
||||||
constructor(params: { request: NextRequest; page: string }) {
|
|
||||||
//@ts-ignore
|
|
||||||
super(params.request);
|
|
||||||
this.sourcePage = params.page;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get request() {
|
|
||||||
throw new DeprecationError({
|
|
||||||
page: this.sourcePage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
respondWith() {
|
|
||||||
throw new DeprecationError({
|
|
||||||
page: this.sourcePage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import type { IResult } from 'ua-parser-js';
|
|
||||||
import cookie from 'cookie';
|
|
||||||
import parseua from 'ua-parser-js';
|
|
||||||
import { Request, RequestInit as NodeFetchRequestInit } from 'node-fetch';
|
|
||||||
|
|
||||||
export const INTERNALS = Symbol('internal request');
|
|
||||||
|
|
||||||
export class NextRequest extends Request {
|
|
||||||
[INTERNALS]: {
|
|
||||||
cookieParser(): { [key: string]: string };
|
|
||||||
geo: { city?: string; country?: string; region?: string };
|
|
||||||
ip?: string;
|
|
||||||
page?: { name?: string; params?: { [key: string]: string } };
|
|
||||||
ua?: IResult | null;
|
|
||||||
url: URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(input: Request | string, init: RequestInit = {}) {
|
|
||||||
//@ts-ignore
|
|
||||||
super(input, init);
|
|
||||||
|
|
||||||
const cookieParser = () => {
|
|
||||||
const value = this.headers.get('cookie');
|
|
||||||
return value ? cookie.parse(value) : {};
|
|
||||||
};
|
|
||||||
|
|
||||||
this[INTERNALS] = {
|
|
||||||
cookieParser,
|
|
||||||
geo: init.geo || {},
|
|
||||||
ip: init.ip,
|
|
||||||
page: init.page,
|
|
||||||
url: new URL(typeof input === 'string' ? input : input.url),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public get cookies() {
|
|
||||||
return this[INTERNALS].cookieParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get geo() {
|
|
||||||
return this[INTERNALS].geo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get ip() {
|
|
||||||
return this[INTERNALS].ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get preflight() {
|
|
||||||
return this.headers.get('x-middleware-preflight');
|
|
||||||
}
|
|
||||||
|
|
||||||
public get nextUrl() {
|
|
||||||
return this[INTERNALS].url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get page() {
|
|
||||||
return {
|
|
||||||
name: this[INTERNALS].page?.name,
|
|
||||||
params: this[INTERNALS].page?.params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public get ua() {
|
|
||||||
if (typeof this[INTERNALS].ua !== 'undefined') {
|
|
||||||
return this[INTERNALS].ua || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uaString = this.headers.get('user-agent');
|
|
||||||
if (!uaString) {
|
|
||||||
this[INTERNALS].ua = null;
|
|
||||||
return this[INTERNALS].ua || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this[INTERNALS].ua = {
|
|
||||||
...parseua(uaString),
|
|
||||||
};
|
|
||||||
|
|
||||||
return this[INTERNALS].ua;
|
|
||||||
}
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
public get url() {
|
|
||||||
return this[INTERNALS].url.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestInit extends NodeFetchRequestInit {
|
|
||||||
geo?: {
|
|
||||||
city?: string;
|
|
||||||
country?: string;
|
|
||||||
region?: string;
|
|
||||||
};
|
|
||||||
ip?: string;
|
|
||||||
page?: {
|
|
||||||
name?: string;
|
|
||||||
params?: { [key: string]: string };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import type { CookieSerializeOptions } from 'cookie';
|
|
||||||
import cookie from 'cookie';
|
|
||||||
import { Response, ResponseInit as NodeFetchResponseInit } from 'node-fetch';
|
|
||||||
|
|
||||||
const INTERNALS = Symbol('internal response');
|
|
||||||
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
||||||
|
|
||||||
export class SpecResponse extends Response {
|
|
||||||
[INTERNALS]: {
|
|
||||||
cookieParser(): { [key: string]: string };
|
|
||||||
url?: URL;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(body?: BodyInit | null, init: ResponseInit = {}) {
|
|
||||||
// TODO - why is this failing?
|
|
||||||
// @ts-ignore
|
|
||||||
super(body, init);
|
|
||||||
|
|
||||||
const cookieParser = () => {
|
|
||||||
const value = this.headers.get('cookie');
|
|
||||||
return value ? cookie.parse(value) : {};
|
|
||||||
};
|
|
||||||
|
|
||||||
this[INTERNALS] = {
|
|
||||||
cookieParser,
|
|
||||||
url: init.url ? new URL(init.url) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public get cookies() {
|
|
||||||
return this[INTERNALS].cookieParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
public cookie(
|
|
||||||
name: string,
|
|
||||||
value: { [key: string]: any } | string,
|
|
||||||
opts: CookieSerializeOptions = {}
|
|
||||||
) {
|
|
||||||
const val =
|
|
||||||
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value);
|
|
||||||
|
|
||||||
if (opts.maxAge) {
|
|
||||||
opts.expires = new Date(Date.now() + opts.maxAge);
|
|
||||||
opts.maxAge /= 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.path == null) {
|
|
||||||
opts.path = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.headers.append(
|
|
||||||
'Set-Cookie',
|
|
||||||
cookie.serialize(name, String(val), opts)
|
|
||||||
);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCookie(name: string, opts: CookieSerializeOptions = {}) {
|
|
||||||
return this.cookie(name, '', { expires: new Date(1), path: '/', ...opts });
|
|
||||||
}
|
|
||||||
|
|
||||||
static redirect(url: string | URL, status = 302) {
|
|
||||||
if (!REDIRECTS.has(status)) {
|
|
||||||
throw new RangeError(
|
|
||||||
'Failed to execute "redirect" on "response": Invalid status code'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SpecResponse(null, {
|
|
||||||
headers: { Location: typeof url === 'string' ? url : url.toString() },
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static rewrite(destination: string | URL) {
|
|
||||||
return new SpecResponse(null, {
|
|
||||||
headers: {
|
|
||||||
'x-middleware-rewrite':
|
|
||||||
typeof destination === 'string'
|
|
||||||
? destination
|
|
||||||
: destination.toString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static next() {
|
|
||||||
return new SpecResponse(null, {
|
|
||||||
headers: {
|
|
||||||
'x-middleware-next': '1',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResponseInit extends NodeFetchResponseInit {
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Response } from 'node-fetch';
|
|
||||||
|
|
||||||
export interface NodeHeaders {
|
|
||||||
[header: string]: string | string[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestData {
|
|
||||||
geo?: {
|
|
||||||
city?: string;
|
|
||||||
country?: string;
|
|
||||||
region?: string;
|
|
||||||
};
|
|
||||||
headers: NodeHeaders;
|
|
||||||
ip?: string;
|
|
||||||
method: string;
|
|
||||||
page?: {
|
|
||||||
name?: string;
|
|
||||||
params?: { [key: string]: string };
|
|
||||||
};
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FetchEventResult {
|
|
||||||
response: Response;
|
|
||||||
waitUntil: Promise<any>;
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import type { NodeHeaders } from './types';
|
|
||||||
import { Headers } from 'node-fetch';
|
|
||||||
|
|
||||||
export async function* streamToIterator<T>(
|
|
||||||
readable: ReadableStream<T>
|
|
||||||
): AsyncIterableIterator<T> {
|
|
||||||
const reader = readable.getReader();
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
if (value) {
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notImplemented(name: string, method: string): any {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get the '${method}' property on '${name}': the property is not implemented`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromNodeHeaders(object: NodeHeaders): Headers {
|
|
||||||
const headers = new Headers();
|
|
||||||
for (const [key, value] of Object.entries(object)) {
|
|
||||||
const values = Array.isArray(value) ? value : [value];
|
|
||||||
for (const v of values) {
|
|
||||||
if (v !== undefined) {
|
|
||||||
headers.append(key, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toNodeHeaders(headers?: Headers): NodeHeaders {
|
|
||||||
const result: NodeHeaders = {};
|
|
||||||
if (headers) {
|
|
||||||
for (const [key, value] of headers.entries()) {
|
|
||||||
result[key] = value;
|
|
||||||
if (key.toLowerCase() === 'set-cookie') {
|
|
||||||
result[key] = splitCookiesString(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
|
|
||||||
that are within a single set-cookie field-value, such as in the Expires portion.
|
|
||||||
This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
|
|
||||||
Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
|
|
||||||
React Native's fetch does this for *every* header, including set-cookie.
|
|
||||||
|
|
||||||
Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
|
|
||||||
Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
|
|
||||||
*/
|
|
||||||
export function splitCookiesString(cookiesString: string) {
|
|
||||||
const cookiesStrings = [];
|
|
||||||
let pos = 0;
|
|
||||||
let start;
|
|
||||||
let ch;
|
|
||||||
let lastComma;
|
|
||||||
let nextStart;
|
|
||||||
let cookiesSeparatorFound;
|
|
||||||
|
|
||||||
function skipWhitespace() {
|
|
||||||
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
return pos < cookiesString.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function notSpecialChar() {
|
|
||||||
ch = cookiesString.charAt(pos);
|
|
||||||
|
|
||||||
return ch !== '=' && ch !== ';' && ch !== ',';
|
|
||||||
}
|
|
||||||
|
|
||||||
while (pos < cookiesString.length) {
|
|
||||||
start = pos;
|
|
||||||
cookiesSeparatorFound = false;
|
|
||||||
|
|
||||||
while (skipWhitespace()) {
|
|
||||||
ch = cookiesString.charAt(pos);
|
|
||||||
if (ch === ',') {
|
|
||||||
// ',' is a cookie separator if we have later first '=', not ';' or ','
|
|
||||||
lastComma = pos;
|
|
||||||
pos += 1;
|
|
||||||
|
|
||||||
skipWhitespace();
|
|
||||||
nextStart = pos;
|
|
||||||
|
|
||||||
while (pos < cookiesString.length && notSpecialChar()) {
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently special character
|
|
||||||
if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') {
|
|
||||||
// we found cookies separator
|
|
||||||
cookiesSeparatorFound = true;
|
|
||||||
// pos is inside the next cookie, so back up and return it.
|
|
||||||
pos = nextStart;
|
|
||||||
cookiesStrings.push(cookiesString.substring(start, lastComma));
|
|
||||||
start = pos;
|
|
||||||
} else {
|
|
||||||
// in param ',' or param separator ';',
|
|
||||||
// we continue from that comma
|
|
||||||
pos = lastComma + 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cookiesSeparatorFound || pos >= cookiesString.length) {
|
|
||||||
cookiesStrings.push(cookiesString.substring(start, cookiesString.length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookiesStrings;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`build() should build simple middleware 1`] = `
|
|
||||||
Object {
|
|
||||||
"pages": Object {
|
|
||||||
"_middleware.js": Object {
|
|
||||||
"env": Array [],
|
|
||||||
"files": Array [
|
|
||||||
"server/pages/_middleware.js",
|
|
||||||
],
|
|
||||||
"name": "pages/_middleware",
|
|
||||||
"page": "/",
|
|
||||||
"regexp": "^/.*$",
|
|
||||||
"runtime": "web",
|
|
||||||
"sortingIndex": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"version": 2,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
99
packages/middleware/test/build.test.ts
vendored
99
packages/middleware/test/build.test.ts
vendored
@@ -1,99 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
import { promises as fsp } from 'fs';
|
|
||||||
import { build } from '../src';
|
|
||||||
import { Response } from 'node-fetch';
|
|
||||||
|
|
||||||
const setupFixture = async (fixture: string) => {
|
|
||||||
const fixturePath = join(__dirname, `fixtures/${fixture}`);
|
|
||||||
await build({
|
|
||||||
workPath: fixturePath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const functionsManifest = JSON.parse(
|
|
||||||
await fsp.readFile(
|
|
||||||
join(fixturePath, '.output/functions-manifest.json'),
|
|
||||||
'utf8'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const outputFile = join(fixturePath, '.output/server/pages/_middleware.js');
|
|
||||||
expect(await fsp.stat(outputFile)).toBeTruthy();
|
|
||||||
require(outputFile);
|
|
||||||
//@ts-ignore
|
|
||||||
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
|
|
||||||
return {
|
|
||||||
middleware,
|
|
||||||
functionsManifest,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 { functionsManifest, middleware } = await setupFixture('simple');
|
|
||||||
|
|
||||||
expect(functionsManifest).toMatchSnapshot();
|
|
||||||
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(
|
|
||||||
(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');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should build simple middleware with env vars', async () => {
|
|
||||||
const expectedEnvVar = 'expected-env-var';
|
|
||||||
const fixture = join(__dirname, 'fixtures/env');
|
|
||||||
process.env.ENV_VAR_SHOULD_BE_DEFINED = expectedEnvVar;
|
|
||||||
await build({
|
|
||||||
workPath: fixture,
|
|
||||||
});
|
|
||||||
// env var should be inlined in the output
|
|
||||||
delete process.env.ENV_VAR_SHOULD_BE_DEFINED;
|
|
||||||
|
|
||||||
const outputFile = join(fixture, '.output/server/pages/_middleware.js');
|
|
||||||
expect(await fsp.stat(outputFile)).toBeTruthy();
|
|
||||||
|
|
||||||
require(outputFile);
|
|
||||||
//@ts-ignore
|
|
||||||
const middleware = global._ENTRIES['middleware_pages/_middleware'].default;
|
|
||||||
expect(typeof middleware).toStrictEqual('function');
|
|
||||||
const handledResponse = await middleware({
|
|
||||||
request: {},
|
|
||||||
});
|
|
||||||
expect(String(handledResponse.response.body)).toEqual(expectedEnvVar);
|
|
||||||
expect(
|
|
||||||
(handledResponse.response as Response).headers.get('x-middleware-next')
|
|
||||||
).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a middleware that runs in strict mode', async () => {
|
|
||||||
const { middleware } = await setupFixture('use-strict');
|
|
||||||
const response = await middleware({
|
|
||||||
request: {},
|
|
||||||
});
|
|
||||||
expect(String(response.response.body)).toEqual('is strict mode? yes');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default req => {
|
|
||||||
return new Response(process.env.ENV_VAR_SHOULD_BE_DEFINED);
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default req => {
|
|
||||||
if (req.url === 'http://google.com') {
|
|
||||||
return new Response('Hi from the edge!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default function (req) {
|
|
||||||
const isStrict = (function () {
|
|
||||||
return !this;
|
|
||||||
})();
|
|
||||||
return new Response('is strict mode? ' + (isStrict ? 'yes' : 'no'));
|
|
||||||
}
|
|
||||||
4
packages/middleware/test/tsconfig.json
vendored
4
packages/middleware/test/tsconfig.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"include": ["*.test.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["esnext", "dom", "dom.iterable"],
|
|
||||||
"target": "es2018",
|
|
||||||
"module": "commonjs",
|
|
||||||
"outDir": "dist",
|
|
||||||
"sourceMap": false,
|
|
||||||
"declaration": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"typeRoots": ["./@types", "./node_modules/@types"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/node-bridge",
|
"name": "@vercel/node-bridge",
|
||||||
"version": "2.1.1-canary.2",
|
"version": "2.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vercel/node",
|
"name": "@vercel/node",
|
||||||
"version": "1.12.2-canary.9",
|
"version": "1.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index",
|
"main": "./dist/index",
|
||||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
"@types/cookie": "0.3.3",
|
"@types/cookie": "0.3.3",
|
||||||
"@types/etag": "1.8.0",
|
"@types/etag": "1.8.0",
|
||||||
"@types/test-listen": "1.1.0",
|
"@types/test-listen": "1.1.0",
|
||||||
"@vercel/build-utils": "2.13.1-canary.1",
|
"@vercel/build-utils": "2.14.0",
|
||||||
"@vercel/ncc": "0.24.0",
|
"@vercel/ncc": "0.24.0",
|
||||||
"@vercel/nft": "0.14.0",
|
"@vercel/nft": "0.17.5",
|
||||||
"@vercel/node-bridge": "2.1.1-canary.2",
|
"@vercel/node-bridge": "2.1.1",
|
||||||
"content-type": "1.0.4",
|
"content-type": "1.0.4",
|
||||||
"cookie": "0.4.0",
|
"cookie": "0.4.0",
|
||||||
"etag": "1.8.1",
|
"etag": "1.8.1",
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ async function compile(
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
preparedFiles: Files;
|
preparedFiles: Files;
|
||||||
shouldAddSourcemapSupport: boolean;
|
shouldAddSourcemapSupport: boolean;
|
||||||
watch: string[];
|
|
||||||
}> {
|
}> {
|
||||||
const inputFiles = new Set<string>([entrypointPath]);
|
const inputFiles = new Set<string>([entrypointPath]);
|
||||||
const preparedFiles: Files = {};
|
const preparedFiles: Files = {};
|
||||||
@@ -198,7 +197,7 @@ async function compile(
|
|||||||
ts: true,
|
ts: true,
|
||||||
mixedModules: true,
|
mixedModules: true,
|
||||||
ignore: config.excludeFiles,
|
ignore: config.excludeFiles,
|
||||||
readFile(fsPath: string): Buffer | string | null {
|
async readFile(fsPath: string): Promise<Buffer | string | null> {
|
||||||
const relPath = relative(baseDir, fsPath);
|
const relPath = relative(baseDir, fsPath);
|
||||||
const cached = sourceCache.get(relPath);
|
const cached = sourceCache.get(relPath);
|
||||||
if (cached) return cached.toString();
|
if (cached) return cached.toString();
|
||||||
@@ -256,11 +255,11 @@ async function compile(
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!symlinkTarget.startsWith('..' + sep) &&
|
!symlinkTarget.startsWith('..' + sep) &&
|
||||||
fileList.indexOf(symlinkTarget) === -1
|
!fileList.has(symlinkTarget)
|
||||||
) {
|
) {
|
||||||
const stats = statSync(resolve(baseDir, symlinkTarget));
|
const stats = statSync(resolve(baseDir, symlinkTarget));
|
||||||
if (stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
fileList.push(symlinkTarget);
|
fileList.add(symlinkTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +272,7 @@ async function compile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile ES Modules into CommonJS
|
// Compile ES Modules into CommonJS
|
||||||
const esmPaths = esmFileList.filter(
|
const esmPaths = [...esmFileList].filter(
|
||||||
file =>
|
file =>
|
||||||
!file.endsWith('.ts') &&
|
!file.endsWith('.ts') &&
|
||||||
!file.endsWith('.tsx') &&
|
!file.endsWith('.tsx') &&
|
||||||
@@ -320,7 +319,6 @@ async function compile(
|
|||||||
return {
|
return {
|
||||||
preparedFiles,
|
preparedFiles,
|
||||||
shouldAddSourcemapSupport,
|
shouldAddSourcemapSupport,
|
||||||
watch: fileList,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +375,7 @@ export async function build({
|
|||||||
|
|
||||||
debug('Tracing input files...');
|
debug('Tracing input files...');
|
||||||
const traceTime = Date.now();
|
const traceTime = Date.now();
|
||||||
const { preparedFiles, shouldAddSourcemapSupport, watch } = await compile(
|
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
|
||||||
workPath,
|
workPath,
|
||||||
baseDir,
|
baseDir,
|
||||||
entrypointPath,
|
entrypointPath,
|
||||||
@@ -433,7 +431,7 @@ export async function build({
|
|||||||
runtime: nodeVersion.runtime,
|
runtime: nodeVersion.runtime,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { output: lambda, watch };
|
return { output: lambda };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareCache({
|
export async function prepareCache({
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"private": false,
|
|
||||||
"name": "vercel-plugin-go",
|
|
||||||
"version": "1.0.0-canary.37",
|
|
||||||
"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.13.1-canary.1",
|
|
||||||
"@vercel/go": "1.2.4-canary.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "*",
|
|
||||||
"typescript": "4.3.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { _experimental_convertRuntimeToPlugin } from '@vercel/build-utils';
|
|
||||||
import * as go from '@vercel/go';
|
|
||||||
|
|
||||||
export const build = _experimental_convertRuntimeToPlugin(
|
|
||||||
go.build,
|
|
||||||
'vercel-plugin-go',
|
|
||||||
'.go'
|
|
||||||
);
|
|
||||||
|
|
||||||
export const startDevServer = go.startDevServer;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
6
packages/plugin-node/.gitignore
vendored
@@ -1,6 +0,0 @@
|
|||||||
/dist
|
|
||||||
/test/fixtures/**/.env
|
|
||||||
/test/fixtures/**/.gitignore
|
|
||||||
/test/fixtures/**/.output
|
|
||||||
/test/fixtures/**/types.d.ts
|
|
||||||
/test/fixtures/11-symlinks/symlink
|
|
||||||
45
packages/plugin-node/@types/zeit__ncc/index.d.ts
vendored
45
packages/plugin-node/@types/zeit__ncc/index.d.ts
vendored
@@ -1,45 +0,0 @@
|
|||||||
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
1
packages/plugin-node/bench/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
lambda
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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' });
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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));
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
function doNothing() {}
|
|
||||||
|
|
||||||
module.exports = (req, res) => {
|
|
||||||
doNothing(req.query.who);
|
|
||||||
doNothing(req.body);
|
|
||||||
doNothing(req.cookies);
|
|
||||||
|
|
||||||
res.end('hello');
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = (req, res) => {
|
|
||||||
res.end('hello');
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "bench",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"express": "4.17.1",
|
|
||||||
"fs-extra": "8.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
# 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=
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
#!/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);
|
|
||||||
});
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "vercel-plugin-node",
|
|
||||||
"version": "1.12.2-canary.41",
|
|
||||||
"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.13.1-canary.1",
|
|
||||||
"@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.1",
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// 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';
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,692 +0,0 @@
|
|||||||
import { fork, spawn } from 'child_process';
|
|
||||||
import { createHash } from 'crypto';
|
|
||||||
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,
|
|
||||||
_experimental_updateFunctionsManifest,
|
|
||||||
_experimental_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';
|
|
||||||
|
|
||||||
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 "use" is defined, but it is not "node" then don't
|
|
||||||
// compile this file.
|
|
||||||
if (config.use && config.use !== '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 =
|
|
||||||
'api-routes-node-' + createHash('sha1').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 _experimental_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 _experimental_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');
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Identify /[param]/ in route string
|
|
||||||
const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/;
|
|
||||||
|
|
||||||
export function isDynamicRoute(route: string): boolean {
|
|
||||||
return TEST_ROUTE.test(route);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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, '/');
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
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
377
packages/plugin-node/test/build.test.ts
vendored
@@ -1,377 +0,0 @@
|
|||||||
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');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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');
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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)
|
|
||||||
);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"now-build": "node now-build.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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`);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"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" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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 });
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = (_, resp) => {
|
|
||||||
resp.writeHead(401);
|
|
||||||
resp.end(`${process.env.RANDOMNESS_ENV_VAR}:content-length`);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 2,
|
|
||||||
"builds": [
|
|
||||||
{ "src": "test1.js", "use": "@vercel/node" },
|
|
||||||
{ "src": "test2.js", "use": "@vercel/node" },
|
|
||||||
{ "src": "test3.js", "use": "@vercel/node" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module.exports = (req, res) => {
|
|
||||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "missing-engines-key-on-purpose"
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user