mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
9 Commits
vercel-plu
...
@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
|
||||
__pycache__
|
||||
.vercel
|
||||
.output
|
||||
.turbo
|
||||
|
||||
1
examples/angular/.gitignore
vendored
1
examples/angular/.gitignore
vendored
@@ -41,4 +41,3 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vercel
|
||||
.output
|
||||
|
||||
1
examples/remix/.gitignore
vendored
1
examples/remix/.gitignore
vendored
@@ -2,7 +2,6 @@ node_modules
|
||||
|
||||
.cache
|
||||
.vercel
|
||||
.output
|
||||
|
||||
public/build
|
||||
api/_build
|
||||
|
||||
1
examples/solidstart/.gitignore
vendored
1
examples/solidstart/.gitignore
vendored
@@ -2,7 +2,6 @@ dist
|
||||
worker
|
||||
.solid
|
||||
.vercel
|
||||
.output
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
1
examples/sveltekit/.gitignore
vendored
1
examples/sveltekit/.gitignore
vendored
@@ -7,4 +7,3 @@ node_modules
|
||||
.env.*
|
||||
!.env.example
|
||||
.vercel
|
||||
.output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.13.1-canary.1",
|
||||
"version": "2.14.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@types/node-fetch": "^2.1.6",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "^2.4.1",
|
||||
"@vercel/frameworks": "0.5.1-canary.21",
|
||||
"@vercel/frameworks": "0.6.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"aggregate-error": "3.0.1",
|
||||
"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[]) {
|
||||
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 FileRef from './file-ref';
|
||||
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
|
||||
import { NodejsLambda } from './nodejs-lambda';
|
||||
import { Prerender } from './prerender';
|
||||
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
|
||||
import getWriteableDirectory from './fs/get-writable-directory';
|
||||
@@ -31,17 +32,18 @@ import {
|
||||
getLatestNodeVersion,
|
||||
getDiscontinuedNodeVersions,
|
||||
} from './fs/node-version';
|
||||
import { NowBuildError } from './errors';
|
||||
import streamToBuffer from './fs/stream-to-buffer';
|
||||
import shouldServe from './should-serve';
|
||||
import debug from './debug';
|
||||
import getIgnoreFilter from './get-ignore-filter';
|
||||
import { getPlatformEnv } from './get-platform-env';
|
||||
|
||||
export {
|
||||
FileBlob,
|
||||
FileFsRef,
|
||||
FileRef,
|
||||
Lambda,
|
||||
NodejsLambda,
|
||||
createLambda,
|
||||
Prerender,
|
||||
download,
|
||||
@@ -69,6 +71,7 @@ export {
|
||||
getLatestNodeVersion,
|
||||
getDiscontinuedNodeVersions,
|
||||
getSpawnOptions,
|
||||
getPlatformEnv,
|
||||
streamToBuffer,
|
||||
shouldServe,
|
||||
debug,
|
||||
@@ -89,11 +92,6 @@ export { detectFramework } from './detect-framework';
|
||||
export { DetectorFilesystem } from './detectors/filesystem';
|
||||
export { readConfigFile } from './fs/read-config-file';
|
||||
export { normalizePath } from './fs/normalize-path';
|
||||
export {
|
||||
_experimental_convertRuntimeToPlugin,
|
||||
_experimental_updateFunctionsManifest,
|
||||
_experimental_updateRoutesManifest,
|
||||
} from './convert-runtime-to-plugin';
|
||||
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
@@ -117,25 +115,3 @@ export const isOfficialRuntime = (desired: string, name?: string): boolean => {
|
||||
export const isStaticRuntime = (name?: string): boolean => {
|
||||
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;
|
||||
}
|
||||
|
||||
interface LambdaOptions {
|
||||
export interface LambdaOptions {
|
||||
files: Files;
|
||||
handler: string;
|
||||
runtime: string;
|
||||
@@ -21,6 +21,10 @@ interface LambdaOptions {
|
||||
environment?: Environment;
|
||||
allowQuery?: string[];
|
||||
regions?: string[];
|
||||
/**
|
||||
* @deprecated Use `files` property instead.
|
||||
*/
|
||||
zipBuffer?: Buffer;
|
||||
}
|
||||
|
||||
interface GetLambdaOptionsFromFunctionOptions {
|
||||
@@ -29,19 +33,19 @@ interface GetLambdaOptionsFromFunctionOptions {
|
||||
}
|
||||
|
||||
export class Lambda {
|
||||
public type: 'Lambda';
|
||||
public files: Files;
|
||||
public handler: string;
|
||||
public runtime: string;
|
||||
public memory?: number;
|
||||
public maxDuration?: number;
|
||||
public environment: Environment;
|
||||
public allowQuery?: string[];
|
||||
public regions?: string[];
|
||||
type: 'Lambda';
|
||||
files: Files;
|
||||
handler: string;
|
||||
runtime: string;
|
||||
memory?: number;
|
||||
maxDuration?: number;
|
||||
environment: Environment;
|
||||
allowQuery?: string[];
|
||||
regions?: string[];
|
||||
/**
|
||||
* @deprecated Use `await lambda.createZip()` instead.
|
||||
*/
|
||||
public zipBuffer?: Buffer;
|
||||
zipBuffer?: Buffer;
|
||||
|
||||
constructor({
|
||||
files,
|
||||
@@ -52,8 +56,11 @@ export class Lambda {
|
||||
environment = {},
|
||||
allowQuery,
|
||||
regions,
|
||||
zipBuffer,
|
||||
}: 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 runtime === 'string', '"runtime" is not a string');
|
||||
assert(typeof environment === 'object', '"environment" is not an object');
|
||||
@@ -90,6 +97,7 @@ export class Lambda {
|
||||
this.environment = environment;
|
||||
this.allowQuery = allowQuery;
|
||||
this.regions = regions;
|
||||
this.zipBuffer = zipBuffer;
|
||||
}
|
||||
|
||||
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 FileFsRef from './file-fs-ref';
|
||||
import { Lambda } from './lambda';
|
||||
|
||||
export interface Env {
|
||||
[name: string]: string | undefined;
|
||||
@@ -368,3 +369,49 @@ export interface ProjectSettings {
|
||||
directoryListing?: 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",
|
||||
"version": "23.1.3-canary.74",
|
||||
"version": "24.0.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -43,14 +43,12 @@
|
||||
"node": ">= 12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.13.1-canary.1",
|
||||
"@vercel/go": "1.2.4-canary.6",
|
||||
"@vercel/node": "1.12.2-canary.9",
|
||||
"@vercel/python": "2.1.2-canary.4",
|
||||
"@vercel/ruby": "1.2.10-canary.2",
|
||||
"update-notifier": "4.1.0",
|
||||
"vercel-plugin-middleware": "0.0.0-canary.26",
|
||||
"vercel-plugin-node": "1.12.2-canary.41"
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/go": "1.3.0",
|
||||
"@vercel/node": "1.13.0",
|
||||
"@vercel/python": "2.2.0",
|
||||
"@vercel/ruby": "1.3.0",
|
||||
"update-notifier": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "11.1.2",
|
||||
@@ -90,11 +88,11 @@
|
||||
"@types/update-notifier": "5.1.0",
|
||||
"@types/which": "1.3.2",
|
||||
"@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/frameworks": "0.5.1-canary.21",
|
||||
"@vercel/frameworks": "0.6.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.17.0",
|
||||
"@vercel/nft": "0.17.5",
|
||||
"@zeit/fun": "0.11.2",
|
||||
"@zeit/source-map-support": "0.6.2",
|
||||
"ajv": "6.12.2",
|
||||
|
||||
@@ -56,10 +56,6 @@ const help = () => {
|
||||
|
||||
${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.cyan(`$ ${pkgName} bisect --run ./test.sh`)}
|
||||
@@ -201,7 +197,11 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
if (badDeployment.target !== goodDeployment.target) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
['billing', 'billing'],
|
||||
['bisect', 'bisect'],
|
||||
['build', 'build'],
|
||||
['cc', 'billing'],
|
||||
['cert', 'certs'],
|
||||
['certs', 'certs'],
|
||||
|
||||
@@ -161,7 +161,8 @@ const main = async () => {
|
||||
// * a subcommand (as in: `vercel ls`)
|
||||
const targetOrSubcommand = argv._[2];
|
||||
|
||||
if (targetOrSubcommand === 'build') {
|
||||
const betaCommands: string[] = [];
|
||||
if (betaCommands.includes(targetOrSubcommand)) {
|
||||
console.log(
|
||||
`${chalk.grey(
|
||||
`${getTitleName()} CLI ${
|
||||
@@ -292,14 +293,7 @@ const main = async () => {
|
||||
|
||||
let authConfig = null;
|
||||
|
||||
const subcommandsWithoutToken = [
|
||||
'login',
|
||||
'logout',
|
||||
'help',
|
||||
'init',
|
||||
'update',
|
||||
'build',
|
||||
];
|
||||
const subcommandsWithoutToken = ['login', 'logout', 'help', 'init', 'update'];
|
||||
|
||||
if (authConfigExists) {
|
||||
try {
|
||||
@@ -406,33 +400,20 @@ const main = async () => {
|
||||
} else if (commands.has(singular)) {
|
||||
alternative = singular;
|
||||
}
|
||||
if (targetOrSubcommand === 'build') {
|
||||
output.note(
|
||||
`If you wish to deploy the ${fileType} ${param(
|
||||
targetOrSubcommand
|
||||
)}, run ${getCommandName('deploy build')}.` +
|
||||
console.error(
|
||||
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.`
|
||||
: '')
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
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;
|
||||
}
|
||||
)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (subcommandExists) {
|
||||
@@ -630,9 +611,6 @@ const main = async () => {
|
||||
case 'bisect':
|
||||
func = await import('./commands/bisect');
|
||||
break;
|
||||
case 'build':
|
||||
func = await import('./commands/build');
|
||||
break;
|
||||
case 'certs':
|
||||
func = await import('./commands/certs');
|
||||
break;
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
detectApiExtensions,
|
||||
spawnCommand,
|
||||
isOfficialRuntime,
|
||||
detectFileSystemAPI,
|
||||
} from '@vercel/build-utils';
|
||||
import frameworkList from '@vercel/frameworks';
|
||||
|
||||
@@ -90,7 +89,6 @@ import {
|
||||
} from './types';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
import { loadCliPlugins } from '../plugins';
|
||||
|
||||
const frontendRuntimeSet = new Set(
|
||||
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
|
||||
@@ -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 (this.devCommand) {
|
||||
builders = builders.filter(filterFrontendBuilds);
|
||||
@@ -1377,6 +1349,7 @@ export default class DevServer {
|
||||
return false;
|
||||
};
|
||||
|
||||
/*
|
||||
runDevMiddleware = async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
@@ -1400,6 +1373,7 @@ export default class DevServer {
|
||||
};
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serve project directory as a v2 deployment.
|
||||
@@ -1468,6 +1442,7 @@ export default class DevServer {
|
||||
let prevUrl = req.url;
|
||||
let prevHeaders: HttpHeadersConfig = {};
|
||||
|
||||
/*
|
||||
const middlewareResult = await this.runDevMiddleware(req, res);
|
||||
|
||||
if (middlewareResult) {
|
||||
@@ -1497,6 +1472,7 @@ export default class DevServer {
|
||||
prevUrl = url.format(origUrl);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
for (const phase of phases) {
|
||||
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);
|
||||
|
||||
export const VERCEL_DIR = '.vercel';
|
||||
export const VERCEL_OUTPUT_DIR = '.output';
|
||||
export const VERCEL_DIR_FALLBACK = '.now';
|
||||
export const VERCEL_DIR_README = 'README.txt';
|
||||
export const VERCEL_DIR_PROJECT = 'project.json';
|
||||
@@ -256,13 +255,6 @@ export async function linkFolderToProject(
|
||||
contentModified = true;
|
||||
}
|
||||
|
||||
if (!gitIgnore.split(EOL).includes(VERCEL_OUTPUT_DIR)) {
|
||||
gitIgnore += `${
|
||||
gitIgnore.endsWith(EOL) || gitIgnore.length === 0 ? '' : EOL
|
||||
}${VERCEL_OUTPUT_DIR}${EOL}`;
|
||||
contentModified = true;
|
||||
}
|
||||
|
||||
if (contentModified) {
|
||||
await writeFile(gitIgnorePath, gitIgnore);
|
||||
isGitIgnoreUpdated = true;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
.next
|
||||
yarn.lock
|
||||
.vercel
|
||||
.output
|
||||
.vercel
|
||||
@@ -367,7 +367,7 @@ module.exports = async function prepare(session, binaryPath) {
|
||||
},
|
||||
'project-link-gitignore': {
|
||||
'package.json': '{}',
|
||||
'.gitignore': '.output',
|
||||
'.gitignore': '',
|
||||
},
|
||||
'project-link-legacy': {
|
||||
'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 => {
|
||||
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
|
||||
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
|
||||
t.is(
|
||||
@@ -3353,7 +3297,7 @@ test('[vc link] should show prompts to set up project', async t => {
|
||||
|
||||
// Ensure .gitignore is created
|
||||
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
|
||||
t.is(
|
||||
@@ -3388,7 +3332,7 @@ test('[vc link --confirm] should not show prompts and autolink', async t => {
|
||||
|
||||
// Ensure .gitignore is created
|
||||
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
|
||||
t.is(
|
||||
@@ -3423,7 +3367,7 @@ test('[vc link] should not duplicate paths in .gitignore', async t => {
|
||||
|
||||
// Ensure .gitignore is created
|
||||
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 => {
|
||||
@@ -3447,7 +3391,7 @@ test('[vc dev] should show prompts to set up project', async t => {
|
||||
|
||||
// Ensure .gitignore is created
|
||||
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
|
||||
t.is(
|
||||
@@ -3514,7 +3458,7 @@ test('[vc link] should show project prompts but not framework when `builds` defi
|
||||
|
||||
// Ensure .gitignore is created
|
||||
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
|
||||
t.is(
|
||||
|
||||
@@ -336,6 +336,7 @@ describe('DevServer', () => {
|
||||
})
|
||||
);
|
||||
|
||||
/*
|
||||
it(
|
||||
'should support edge middleware',
|
||||
testFixture('edge-middleware', async server => {
|
||||
@@ -394,4 +395,5 @@ describe('DevServer', () => {
|
||||
expect(body).toStrictEqual('is strict mode? yes');
|
||||
})
|
||||
);
|
||||
*/
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "10.2.3-canary.52",
|
||||
"version": "10.3.0",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -40,7 +40,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.13.1-canary.1",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/frameworks",
|
||||
"version": "0.5.1-canary.21",
|
||||
"version": "0.6.0",
|
||||
"main": "./dist/frameworks.js",
|
||||
"types": "./dist/frameworks.d.ts",
|
||||
"files": [
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/js-yaml": "3.12.1",
|
||||
"@types/node": "12.0.4",
|
||||
"@types/node-fetch": "2.5.8",
|
||||
"@vercel/routing-utils": "1.11.4-canary.6",
|
||||
"@vercel/routing-utils": "1.12.0",
|
||||
"ajv": "6.12.2",
|
||||
"typescript": "4.3.4"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/go",
|
||||
"version": "1.2.4-canary.6",
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/fs-extra": "^5.0.5",
|
||||
"@types/node-fetch": "^2.3.0",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@vercel/build-utils": "2.13.1-canary.1",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"async-retry": "1.3.1",
|
||||
"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",
|
||||
"version": "2.1.1-canary.2",
|
||||
"version": "2.1.1",
|
||||
"license": "MIT",
|
||||
"main": "./index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node",
|
||||
"version": "1.12.2-canary.9",
|
||||
"version": "1.13.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -32,10 +32,10 @@
|
||||
"@types/cookie": "0.3.3",
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"@vercel/build-utils": "2.13.1-canary.1",
|
||||
"@vercel/build-utils": "2.14.0",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"@vercel/nft": "0.14.0",
|
||||
"@vercel/node-bridge": "2.1.1-canary.2",
|
||||
"@vercel/nft": "0.17.5",
|
||||
"@vercel/node-bridge": "2.1.1",
|
||||
"content-type": "1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"etag": "1.8.1",
|
||||
|
||||
@@ -135,7 +135,6 @@ async function compile(
|
||||
): Promise<{
|
||||
preparedFiles: Files;
|
||||
shouldAddSourcemapSupport: boolean;
|
||||
watch: string[];
|
||||
}> {
|
||||
const inputFiles = new Set<string>([entrypointPath]);
|
||||
const preparedFiles: Files = {};
|
||||
@@ -198,7 +197,7 @@ async function compile(
|
||||
ts: true,
|
||||
mixedModules: true,
|
||||
ignore: config.excludeFiles,
|
||||
readFile(fsPath: string): Buffer | string | null {
|
||||
async readFile(fsPath: string): Promise<Buffer | string | null> {
|
||||
const relPath = relative(baseDir, fsPath);
|
||||
const cached = sourceCache.get(relPath);
|
||||
if (cached) return cached.toString();
|
||||
@@ -256,11 +255,11 @@ async function compile(
|
||||
);
|
||||
if (
|
||||
!symlinkTarget.startsWith('..' + sep) &&
|
||||
fileList.indexOf(symlinkTarget) === -1
|
||||
!fileList.has(symlinkTarget)
|
||||
) {
|
||||
const stats = statSync(resolve(baseDir, symlinkTarget));
|
||||
if (stats.isFile()) {
|
||||
fileList.push(symlinkTarget);
|
||||
fileList.add(symlinkTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +272,7 @@ async function compile(
|
||||
}
|
||||
|
||||
// Compile ES Modules into CommonJS
|
||||
const esmPaths = esmFileList.filter(
|
||||
const esmPaths = [...esmFileList].filter(
|
||||
file =>
|
||||
!file.endsWith('.ts') &&
|
||||
!file.endsWith('.tsx') &&
|
||||
@@ -320,7 +319,6 @@ async function compile(
|
||||
return {
|
||||
preparedFiles,
|
||||
shouldAddSourcemapSupport,
|
||||
watch: fileList,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,7 +375,7 @@ export async function build({
|
||||
|
||||
debug('Tracing input files...');
|
||||
const traceTime = Date.now();
|
||||
const { preparedFiles, shouldAddSourcemapSupport, watch } = await compile(
|
||||
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
|
||||
workPath,
|
||||
baseDir,
|
||||
entrypointPath,
|
||||
@@ -433,7 +431,7 @@ export async function build({
|
||||
runtime: nodeVersion.runtime,
|
||||
});
|
||||
|
||||
return { output: lambda, watch };
|
||||
return { output: lambda };
|
||||
}
|
||||
|
||||
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