Compare commits

...

24 Commits

Author SHA1 Message Date
Sean Massa
b30f000d2a Publish Stable
- vercel@28.16.4
 - @vercel/next@3.5.2
2023-02-21 18:51:33 -06:00
JJ Kasper
f78051ada9 [next] Ensure .rsc outputs are not created for route handlers (#9503) 2023-02-21 16:41:54 -08:00
Don Alvarez
10bc74904c [docs] Improve docs for @vercel/next no pages built message (#9494)
After many hours of debugging, I tracked down that having an old Node
version (eg 14.x) listed in your Vercel project settings can result in
the build step failing with a confusing and unhelpful error message
"`@vercel/next` No Serverless Pages Built". Note that this is a case
where it "can" cause it to fail, including with NextJS 13.1.6 and Vercel
CLI 28.16.2, but it is not guaranteed to fail. I have six NextJS
projects. They have identical next.config.js, tsconfig.json,
eslintrc.js, and .gitignore files, and other than a few seemingly
non-critical dependencies they have identical package.json files. Four
of the six consistently built and deployed in the cloud without issue.
Two consistently failed to build in the cloud. All built successfully
locally including using vercel build locally, and all would vercel
deploy --prebuilt successfully. Switching all the vercel cloud project
settings from Node 14.x to Node 18.x enabled all the projects to build
and deploy successfully in the vercel cloud without needing local vercel
build and local vercel deploy --prebuilt steps.

---------

Co-authored-by: Steven <steven@ceriously.com>
2023-02-21 11:32:03 -05:00
Steven
c8f7a9a874 [cli] Fix global detection for fnm (#9496)
Fix `--global` install detection when
[fnm](https://github.com/Schniz/fnm) was used to install node
2023-02-21 10:11:08 -05:00
Sean Massa
2fd3315221 Publish Stable
- vercel@28.16.3
 - @vercel/next@3.5.1
 - @vercel/remix@1.3.5
2023-02-21 08:53:23 -06:00
JJ Kasper
54ef027cbe [next] Fix rsc routes order (#9493)
This ensures we maintain the correct order for our rsc routes with
reference to middleware so that they match correctly. This also adds a
regression test to ensure it's working as expected.

Fixes: https://github.com/vercel/next.js/issues/45331
x-ref: [slack
thread](https://vercel.slack.com/archives/C035J346QQL/p1676926522772859?thread_ts=1676926096.412539&cid=C035J346QQL)
2023-02-20 17:54:17 -08:00
Shu Ding
6620c7f600 [next] Add initial vary header to all prerendered pages when RSC is enabled (#9481)
This PR changes the fallback headers that relate to RSC to the defaults that Next.js currently uses. Also, it sets the initial `vary` header to all prerendered routes when RSC is enabled (`routesManifest?.rsc`), even for the pages directory. 

That's because although the pages directory won't return any RSC payload, it can still be used in a project that contains app routes. When the app route requests a page route for RSC data, it's important for the browser to not accidentally cache that result hence we need the `vary` header to set there as well.

More related discussions can be found [here](https://linear.app/vercel/issue/NEXT-382/add-vary-rsc-etc-header-to-all-responses-to-ensure-browser-caching).
2023-02-20 19:35:06 +00:00
JJ Kasper
38f40f1c15 [next] Handle prerender-manifest v4 (#9489)
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
2023-02-20 11:41:24 +01:00
Nathan Rajlich
63211b8b89 [remix] Add unit tests (#9469)
Moves parts of the `@vercel/remix` builder into util functions that have isolated unit tests. No functionality changes.
2023-02-17 18:46:33 +00:00
Ikko Eltociear Ashimine
83ee5ea2b8 [cli] Fix typo in get-latest-worker.js (#9470)
persistance -> persistence

Co-authored-by: Chris Barber <chris.barber@vercel.com>
2023-02-17 11:45:44 -05:00
Ethan Arrowood
f063645646 [examples] Update remix template (#9472)
Previous PR was merged automatically but need to fix a dependency
version
2023-02-17 09:42:26 -07:00
Ethan Arrowood
4f8c5e344d [examples] update remix template (#9455)
Updates our remix template so that it works with our new Remix changes
2023-02-17 15:10:17 +00:00
Nathan Rajlich
70a53515bd Publish Stable
- vercel@28.16.2
 - @vercel/fs-detectors@3.8.0
 - @vercel/next@3.5.0
 - @vercel/remix@1.3.4
 - @vercel/ruby@1.3.66
 - @vercel/static-build@1.3.10
2023-02-16 15:12:31 -08:00
Sean Massa
4d4f0fa672 [next] Add Operation Types to Next.js Lambdas (#9196)
In order to have Next.js Lambdas show their operation types more specifically in the build output in the dashboard, the builder needs to return the Lambdas with `operationType` set to the appropriate value.

This PR adds those values. This allows the Richer Deployment Outputs to show the different types of serverless functions:
<img width="228" alt="Screenshot 2023-02-03 at 3 49 42 PM" src="https://user-images.githubusercontent.com/41545/216717479-d02fbd4a-fa62-479d-8b65-bd77fdcdb26c.png">
2023-02-16 22:59:14 +00:00
Felix Haus
46c0fd153a [fs-detectors] Remove increments of 64 limit for function memory (#9465)
Missed this occurrence so it still prevents the upload of serverless
functions that have a mem value that is not dividable by 64.
Should be the last place before we can ship the documentation update.

#### Related PRs
- #9440

Co-authored-by: Steven <steven@ceriously.com>
2023-02-16 16:42:42 -05:00
Marc Greenstock
1c8b4717e3 [ruby] fix: HEAD requests (#9436)
When WEBrick receives `HEAD` requests it discards the body (i.e.
`req.body.nil? => true`), this causes Vercel to throw a
`BODY_NOT_A_STRING_FROM_FUNCTION` since it is expecting the serverless
function to respond with a string in the body.

---------

Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Steven <steven@ceriously.com>
2023-02-16 16:41:56 -05:00
Nathan Rajlich
d52d26eaac [remix] Install Node globals (#9467)
Fixes https://github.com/vercel/community/discussions/1547.
Fixes https://github.com/vercel/community/discussions/1549.
2023-02-16 21:25:19 +00:00
Vincent Voyer
db65728fc4 Publish Stable
- vercel@28.16.1
2023-02-16 15:36:16 +01:00
Vincent Voyer
a788d06f85 [cli]: fix merging of vercel.json and build result crons (#9464)
Ensures that existing crons and crons from vercel.json are merged
together correctly.
2023-02-16 15:34:56 +01:00
Vincent Voyer
3d98d1cdea Publish Stable
- @vercel/build-utils@6.3.0
 - vercel@28.16.0
 - @vercel/client@12.4.0
 - @vercel/fs-detectors@3.7.14
 - @vercel/gatsby-plugin-vercel-builder@1.1.7
 - @vercel/go@2.3.7
 - @vercel/hydrogen@0.0.53
 - @vercel/next@3.4.7
 - @vercel/node@2.9.6
 - @vercel/python@3.1.49
 - @vercel/redwood@1.1.5
 - @vercel/remix@1.3.3
 - @vercel/ruby@1.3.65
 - @vercel/static-build@1.3.9
 - @vercel/static-config@2.0.13
2023-02-16 12:08:50 +01:00
Vincent Voyer
667af829c4 [build-utils][cli][client][node][next][static-config]: forward crons from vercel.json to config.json (#9454)
This PR changes the way cron jobs are being created in the build output
API. This is my first time contributing here. If you see something
unusual, let me know.

 Good for review

Our goal is to:
- Allow creating cron jobs via the `crons` property of `vercel.json` for
end users
- Allow framework authors to create cron jobs on Vercel via the `crons`
property of the Build Output API configuration

---

As you can see, we removed the previous implementation where cron jobs
could be configured at the function code level (export const cron = ""),
on top of vercel.json `functions` property. Here's why:

- All frameworks would have to implement the configure at the function
code level
- Not all frameworks can easily map a path to a specific function
(example: SvelteKit) and would have to bail on bundling functions inside
the same lambda
- Configuring a path + scheduler provides a better mapping of what cron
jobs are as of today: API routes on a schedule and not functions on a
schedule
- Dynamic routes Cron Jobs will be supported:
/api/crons/sync-slack-team/230
- Query parameters will be supported support:
/api/crons/sync-slack-team/230?secret=32k13l2k13lk21 (= securing cron
jobs v0)
- 100% frameworks compatibility from day one

Next.js and other frameworks may choose to implement their own cron jobs
feature that will then need to be configured through the `crons`
property of `config.json` (build output API).

cc @timneutkens @Rich-Harris 

Internal thread:
https://vercel.slack.com/archives/C04DWF5HB6K/p1676366892714349
2023-02-16 11:49:09 +01:00
Nathan Rajlich
1bb7b37e0c Publish Stable
- vercel@28.15.7
 - @vercel/next@3.4.6
 - @vercel/remix@1.3.2
2023-02-15 23:41:25 -08:00
Nathan Rajlich
ca81c133b9 [remix] Fix root-level splat path (#9457)
Fixes https://github.com/orgs/vercel/discussions/1545.
2023-02-16 07:32:53 +00:00
Luba Kravchenko
f332b7856c Adds framework fields to build outputs (#9449)
Adds framework to Lambda and edge build outputs so that we can distinguish which framework they originated from when certain features should be applied to specific frameworks.

This PR adds framework to the outputs. Part 1: https://github.com/vercel/vercel/pull/9448

ticket: ED-131

x-ref: [slack channel](https://vercel.slack.com/archives/C042LHPJ1NX)
2023-02-15 22:56:29 +00:00
82 changed files with 2537 additions and 2628 deletions

View File

@@ -14,7 +14,9 @@ In order to create the smallest possible lambdas Next.js has to be configured to
npm install next --save
```
2. Add the `now-build` script to your `package.json`
2. Check [Node.js Version](https://vercel.link/node-version) in your Project Settings. Using an old or incompatible version of Node.js can cause the Build Step to fail with this error message.
3. Add the `now-build` script to your `package.json` [deprecated]
```json
{
@@ -24,7 +26,7 @@ npm install next --save
}
```
3. Add `target: 'serverless'` to `next.config.js` [deprecated]
4. Add `target: 'serverless'` to `next.config.js` [deprecated]
```js
module.exports = {
@@ -33,9 +35,9 @@ module.exports = {
};
```
4. Remove `distDir` from `next.config.js` as `@vercel/next` can't parse this file and expects your build output at `/.next`
5. Remove `distDir` from `next.config.js` as `@vercel/next` can't parse this file and expects your build output at `/.next`
5. Optionally make sure the `"src"` in `"builds"` points to your application `package.json`
6. Optionally make sure the `"src"` in `"builds"` points to your application `package.json`
```js
{

View File

@@ -13,10 +13,10 @@ function hydrate() {
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
if (typeof requestIdleCallback === "function") {
requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
setTimeout(hydrate, 1);
}

View File

@@ -1,21 +1,14 @@
import type { EntryContext } from "@remix-run/node";
import handleRequest from "@vercel/remix-entry-server";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import type { EntryContext } from "@remix-run/server-runtime";
export default function handleRequest(
export default function (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
headers: responseHeaders,
status: responseStatusCode,
});
const remixServer = <RemixServer context={remixContext} url={request.url} />;
return handleRequest(request, responseStatusCode, responseHeaders, remixServer)
}

View File

@@ -0,0 +1,11 @@
export const config = {
runtime: 'edge'
};
export default function Edge() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix@Edge</h1>
</div>
);
}

View File

@@ -6,20 +6,20 @@
"dev": "remix dev"
},
"dependencies": {
"@remix-run/node": "^1.7.6",
"@remix-run/react": "^1.7.6",
"@remix-run/vercel": "^1.7.6",
"@vercel/analytics": "^0.1.5",
"@vercel/node": "^2.7.0",
"@remix-run/node": "^1.13.0",
"@remix-run/react": "^1.13.0",
"@remix-run/serve": "^1.13.0",
"@remix-run/server-runtime": "^1.13.0",
"@vercel/analytics": "^0.1.10",
"@vercel/remix-entry-server": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@remix-run/dev": "^1.7.6",
"@remix-run/eslint-config": "^1.7.6",
"@remix-run/serve": "^1.7.6",
"@remix-run/dev": "^1.13.0",
"@remix-run/eslint-config": "^1.13.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-dom": "^18.0.11",
"eslint": "^8.28.0",
"typescript": "^4.9.3"
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
/** @type {import('@remix-run/dev').AppConfig} */
/**
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
serverBuildTarget: "vercel",
// When running locally in development mode, we use the built in remix
// server. This does not understand the vercel lambda module format,
// so we default back to the standard build output.
server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
ignoredRouteFiles: ["**/.*"],
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "api/index.js",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
};

View File

@@ -1,4 +0,0 @@
import { createRequestHandler } from "@remix-run/vercel";
import * as build from "@remix-run/dev/server-build";
export default createRequestHandler({ build, mode: process.env.NODE_ENV });

View File

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

View File

@@ -1,4 +1,4 @@
import type { Cron, Files, FunctionFramework } from './types';
import type { Files, FunctionFramework } from './types';
/**
* An Edge Functions output
@@ -41,9 +41,6 @@ export class EdgeFunction {
/** The regions where the edge function will be executed on */
regions?: string | string[];
/** Cronjob definition for the edge function */
cron?: Cron;
/** The framework */
framework?: FunctionFramework;
@@ -56,7 +53,6 @@ export class EdgeFunction {
this.envVarsInUse = params.envVarsInUse;
this.assets = params.assets;
this.regions = params.regions;
this.cron = params.cron;
this.framework = params.framework;
}
}

View File

@@ -5,7 +5,7 @@ import minimatch from 'minimatch';
import { readlink } from 'fs-extra';
import { isSymbolicLink, isDirectory } from './fs/download';
import streamToBuffer from './fs/stream-to-buffer';
import type { Files, Config, Cron, FunctionFramework } from './types';
import type { Files, Config, FunctionFramework } from './types';
interface Environment {
[key: string]: string;
@@ -25,7 +25,6 @@ export interface LambdaOptionsBase {
supportsWrapper?: boolean;
experimentalResponseStreaming?: boolean;
operationType?: string;
cron?: Cron;
framework?: FunctionFramework;
}
@@ -64,7 +63,6 @@ export class Lambda {
environment: Environment;
allowQuery?: string[];
regions?: string[];
cron?: Cron;
/**
* @deprecated Use `await lambda.createZip()` instead.
*/
@@ -83,7 +81,6 @@ export class Lambda {
environment = {},
allowQuery,
regions,
cron,
supportsMultiPayloads,
supportsWrapper,
experimentalResponseStreaming,
@@ -138,10 +135,6 @@ export class Lambda {
);
}
if (cron !== undefined) {
assert(typeof cron === 'string', '"cron" is not a string');
}
if (framework !== undefined) {
assert(typeof framework === 'object', '"framework" is not an object');
assert(
@@ -166,7 +159,6 @@ export class Lambda {
this.environment = environment;
this.allowQuery = allowQuery;
this.regions = regions;
this.cron = cron;
this.zipBuffer = 'zipBuffer' in opts ? opts.zipBuffer : undefined;
this.supportsMultiPayloads = supportsMultiPayloads;
this.supportsWrapper = supportsWrapper;
@@ -246,7 +238,7 @@ export async function getLambdaOptionsFromFunction({
sourceFile,
config,
}: GetLambdaOptionsFromFunctionOptions): Promise<
Pick<LambdaOptions, 'memory' | 'maxDuration' | 'cron'>
Pick<LambdaOptions, 'memory' | 'maxDuration'>
> {
if (config?.functions) {
for (const [pattern, fn] of Object.entries(config.functions)) {
@@ -254,7 +246,6 @@ export async function getLambdaOptionsFromFunction({
return {
memory: fn.memory,
maxDuration: fn.maxDuration,
cron: fn.cron,
};
}
}

View File

@@ -29,11 +29,6 @@ export const functionsSchema = {
type: 'string',
maxLength: 256,
},
cron: {
type: 'string',
minLength: 9,
maxLength: 256,
},
},
},
},

View File

@@ -319,7 +319,6 @@ export interface BuilderFunctions {
runtime?: string;
includeFiles?: string;
excludeFiles?: string;
cron?: Cron;
};
}
@@ -411,7 +410,11 @@ export interface BuildResultBuildOutput {
buildOutputPath: string;
}
export type Cron = string;
export interface Cron {
path: string;
schedule: string;
}
/** The framework which created the function */
export interface FunctionFramework {
slug: string;

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.15.6",
"version": "28.16.4",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -41,16 +41,16 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "6.2.4",
"@vercel/go": "2.3.6",
"@vercel/hydrogen": "0.0.52",
"@vercel/next": "3.4.5",
"@vercel/node": "2.9.5",
"@vercel/python": "3.1.48",
"@vercel/redwood": "1.1.4",
"@vercel/remix": "1.3.1",
"@vercel/ruby": "1.3.64",
"@vercel/static-build": "1.3.8"
"@vercel/build-utils": "6.3.0",
"@vercel/go": "2.3.7",
"@vercel/hydrogen": "0.0.53",
"@vercel/next": "3.5.2",
"@vercel/node": "2.9.6",
"@vercel/python": "3.1.49",
"@vercel/redwood": "1.1.5",
"@vercel/remix": "1.3.5",
"@vercel/ruby": "1.3.66",
"@vercel/static-build": "1.3.10"
},
"devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5",
@@ -93,10 +93,10 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.3.10",
"@vercel/client": "12.4.0",
"@vercel/error-utils": "1.0.8",
"@vercel/frameworks": "1.3.1",
"@vercel/fs-detectors": "3.7.13",
"@vercel/fs-detectors": "3.8.0",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.1.9",

View File

@@ -16,6 +16,7 @@ import {
BuildResultV2Typical,
BuildResultV3,
NowBuildError,
Cron,
} from '@vercel/build-utils';
import {
detectBuilders,
@@ -88,6 +89,7 @@ interface BuildOutputConfig {
framework?: {
version: string;
};
crons?: Cron[];
}
/**
@@ -623,6 +625,7 @@ async function doBuild(
});
const mergedImages = mergeImages(localConfig.images, buildResults.values());
const mergedCrons = mergeCrons(localConfig.crons, buildResults.values());
const mergedWildcard = mergeWildcard(buildResults.values());
const mergedOverrides: Record<string, PathOverride> =
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
@@ -638,6 +641,7 @@ async function doBuild(
wildcard: mergedWildcard,
overrides: mergedOverrides,
framework,
crons: mergedCrons,
};
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
@@ -746,6 +750,18 @@ function mergeImages(
return images;
}
function mergeCrons(
crons: BuildOutputConfig['crons'] = [],
buildResults: Iterable<BuildResult | BuildOutputConfig>
): BuildOutputConfig['crons'] {
for (const result of buildResults) {
if ('crons' in result && result.crons) {
crons = crons.concat(result.crons);
}
}
return crons;
}
function mergeWildcard(
buildResults: Iterable<BuildResult | BuildOutputConfig>
): BuildResultV2Typical['wildcard'] {

View File

@@ -13,7 +13,6 @@ import {
Builder,
BuildResultV2,
BuildResultV3,
Cron,
File,
FileFsRef,
BuilderV2,
@@ -41,7 +40,6 @@ export const OUTPUT_DIR = join(VERCEL_DIR, 'output');
* An entry in the "functions" object in `vercel.json`.
*/
interface FunctionConfiguration {
cron?: Cron;
memory?: number;
maxDuration?: number;
}
@@ -372,14 +370,12 @@ async function writeLambda(
throw new Error('Malformed `Lambda` - no "files" present');
}
const cron = functionConfiguration?.cron ?? lambda.cron;
const memory = functionConfiguration?.memory ?? lambda.memory;
const maxDuration = functionConfiguration?.maxDuration ?? lambda.maxDuration;
const config = {
...lambda,
handler: normalizePath(lambda.handler),
cron,
memory,
maxDuration,
type: undefined,

View File

@@ -22,7 +22,7 @@ const { format, inspect } = require('util');
/**
* An simple output helper which accumulates error and debug log messages in
* memory for potential persistance to disk while immediately outputting errors
* memory for potential persistence to disk while immediately outputting errors
* and debug messages, when the `--debug` flag is set, to `stderr`.
*/
class WorkerOutput {

View File

@@ -76,6 +76,10 @@ async function isGlobal() {
return true;
}
if (installPath.includes(['', 'fnm', 'node-versions', ''].join(sep))) {
return true;
}
const prefixPath =
process.env.PREFIX ||
process.env.npm_config_prefix ||

View File

@@ -93,6 +93,29 @@ const imagesSchema = {
},
};
const cronsSchema = {
type: 'array',
minItems: 0,
items: {
type: 'object',
additionalProperties: false,
required: ['path', 'schedule'],
properties: {
path: {
type: 'string',
minLength: 1,
maxLength: 512,
pattern: '^/.*',
},
schedule: {
type: 'string',
minLength: 9,
maxLength: 256,
},
},
},
};
const vercelConfigSchema = {
type: 'object',
// These are not all possibilities because `vc dev`
@@ -108,6 +131,7 @@ const vercelConfigSchema = {
trailingSlash: trailingSlashSchema,
functions: functionsSchema,
images: imagesSchema,
crons: cronsSchema,
},
};

View File

@@ -0,0 +1,7 @@
{
"orgId": ".",
"projectId": ".",
"settings": {
"framework": null
}
}

View File

@@ -1,3 +1,3 @@
export default function (req, res) {
res.json('hello from the edge');
res.send('Hello from cron job!');
}

View File

@@ -0,0 +1,9 @@
const fs = require('fs');
const path = require('path');
fs.rmSync(path.join(__dirname, '.vercel', 'output'), { recursive: true });
fs.mkdirSync(path.join(__dirname, '.vercel', 'output'));
fs.copyFileSync(
path.join(__dirname, 'output', 'config.json'),
path.join(__dirname, '.vercel', 'output', 'config.json')
);

View File

@@ -0,0 +1 @@
<h1>Vercel</h1>

View File

@@ -0,0 +1,9 @@
{
"version": 3,
"crons": [
{
"path": "/api/cron-job-build-output",
"schedule": "0 0 * * *"
}
]
}

View File

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

View File

@@ -0,0 +1,8 @@
{
"crons": [
{
"path": "/api/cron-job",
"schedule": "0 0 * * *"
}
]
}

View File

@@ -1,3 +1,3 @@
export default function (req, res) {
res.end('serverless says hello');
res.send('Hello from cron job!');
}

View File

@@ -1,17 +0,0 @@
export const config = {
runtime: 'edge',
cron: '* * * * *',
};
export default async function edge(request, event) {
const requestBody = await request.text();
return new Response(
JSON.stringify({
headerContentType: request.headers.get('content-type'),
url: request.url,
method: request.method,
body: requestBody,
})
);
}

View File

@@ -1,7 +0,0 @@
export default function (req, res) {
res.json({ memory: parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE) });
}
export const config = {
cron: '* * * * *',
};

View File

@@ -1,10 +1,8 @@
{
"functions": {
"api/overwrite/serverless.js": {
"cron": "0 10-20 * * *"
},
"api/overwrite/edge.js": {
"cron": "10 * * * *"
"crons": [
{
"path": "/api/cron-job",
"schedule": "0 0 * * *"
}
}
]
}

View File

@@ -2635,7 +2635,7 @@ test('next unsupported functions config shows warning link', async t => {
t.is(output.exitCode, 0, formatOutput(output));
t.regex(
output.stderr,
/Ignoring function property `runtime`\. When using Next\.js, only `memory`, `maxDuration`, and `cron` can be used\./gm,
/Ignoring function property `runtime`\. When using Next\.js, only `memory` and `maxDuration` can be used\./gm,
formatOutput(output)
);
t.regex(

View File

@@ -1104,32 +1104,46 @@ describe('build', () => {
it('should include crons property in build output', async () => {
const cwd = fixture('with-cron');
const output = join(cwd, '.vercel', 'output', 'functions', 'api');
const output = join(cwd, '.vercel', 'output');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toBe(0);
const edge = await fs.readJSON(
join(output, 'edge.func', '.vc-config.json')
);
expect(edge).toHaveProperty('cron', '* * * * *');
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toHaveProperty('crons', [
{
path: '/api/cron-job',
schedule: '0 0 * * *',
},
]);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
const serverless = await fs.readJSON(
join(output, 'serverless.func', '.vc-config.json')
);
expect(serverless).toHaveProperty('cron', '* * * * *');
it('should merge crons property from build output with vercel.json crons property', async () => {
const cwd = fixture('with-cron-merge');
const output = join(cwd, '.vercel', 'output');
const overwriteServerless = await fs.readJSON(
join(output, 'overwrite', 'serverless.func', '.vc-config.json')
);
expect(overwriteServerless).toHaveProperty('cron', '0 10-20 * * *');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toBe(0);
const overwriteEdge = await fs.readJSON(
join(output, 'overwrite', 'edge.func', '.vc-config.json')
);
expect(overwriteEdge).toHaveProperty('cron', '10 * * * *');
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toHaveProperty('crons', [
{
path: '/api/cron-job',
schedule: '0 0 * * *',
},
{
path: '/api/cron-job-build-output',
schedule: '0 0 * * *',
},
]);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;

View File

@@ -285,4 +285,90 @@ describe('validateConfig', () => {
expect(error!.link).toEqual('https://vercel.link/functions-and-builds');
});
it('should error when crons have missing schedule', () => {
const error = validateConfig({
// @ts-ignore
crons: [{ path: '/api/test.js' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0]` missing required property `schedule`.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it('should error when crons have missing path', () => {
const error = validateConfig({
// @ts-ignore
crons: [{ schedule: '* * * * *' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0]` missing required property `path`.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it('should error when path is too long', () => {
const error = validateConfig({
crons: [{ path: '/' + 'x'.repeat(512), schedule: '* * * * *' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0].path` should NOT be longer than 512 characters.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it('should error when schedule is too long', () => {
const error = validateConfig({
crons: [{ path: '/', schedule: '*'.repeat(257) }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0].schedule` should NOT be longer than 256 characters.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it('should error when path is empty', () => {
const error = validateConfig({
crons: [{ path: '', schedule: '* * * * *' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0].path` should NOT be shorter than 1 characters.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it('should error when schedule is too short', () => {
const error = validateConfig({
crons: [{ path: '/', schedule: '* * * * ' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0].schedule` should NOT be shorter than 9 characters.'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
it("should error when path doesn't start with `/`", () => {
const error = validateConfig({
crons: [{ path: 'api/cron', schedule: '* * * * *' }],
});
expect(error!.message).toEqual(
'Invalid vercel.json - `crons[0].path` should match pattern "^/.*".'
);
expect(error!.link).toEqual(
'https://vercel.com/docs/concepts/projects/project-configuration#crons'
);
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.3.10",
"version": "12.4.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -43,7 +43,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/routing-utils": "2.1.9",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",

View File

@@ -3,6 +3,7 @@ import type {
BuilderFunctions,
Images,
ProjectSettings,
Cron,
} from '@vercel/build-utils';
import type { Header, Route, Redirect, Rewrite } from '@vercel/routing-utils';
@@ -154,6 +155,7 @@ export interface VercelConfig {
framework?: string | null;
outputDirectory?: string | null;
images?: Images;
crons?: Cron[];
}
export interface GitMetadata {

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "3.7.13",
"version": "3.8.0",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -35,7 +35,7 @@
"@types/minimatch": "3.0.5",
"@types/node": "14.18.33",
"@types/semver": "7.3.10",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"typescript": "4.3.4"
}
}

View File

@@ -602,12 +602,11 @@ function validateFunctions({ functions = {} }: Options) {
if (
func.memory !== undefined &&
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)
(func.memory < 128 || func.memory > 3008)
) {
return {
code: 'invalid_function_memory',
message:
'Functions must have a memory value between 128 and 3008 in steps of 64.',
message: 'Functions must have a memory value between 128 and 3008',
};
}

View File

@@ -473,7 +473,7 @@ describe('Test `detectBuilders`', () => {
});
it('invalid function memory', async () => {
const functions = { 'pages/index.ts': { memory: 200 } };
const functions = { 'pages/index.ts': { memory: 127 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
@@ -484,6 +484,17 @@ describe('Test `detectBuilders`', () => {
expect(errors![0].code).toBe('invalid_function_memory');
});
it('should build with function memory not dividable by 64', async () => {
const functions = { 'api/index.ts': { memory: 1000 } };
const files = ['api/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders![0].use).toBe('@vercel/node');
expect(errors).toBeNull();
});
it('missing runtime version', async () => {
const functions = { 'pages/index.ts': { runtime: 'haha' } };
const files = ['pages/index.ts'];
@@ -1720,7 +1731,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
});
it('invalid function memory', async () => {
const functions = { 'pages/index.ts': { memory: 200 } };
const functions = { 'pages/index.ts': { memory: 127 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
@@ -1732,6 +1743,18 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errors![0].code).toBe('invalid_function_memory');
});
it('should build with function memory not dividable by 64', async () => {
const functions = { 'api/index.ts': { memory: 1000 } };
const files = ['api/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
featHandleMiss,
});
expect(builders![0].use).toBe('@vercel/node');
expect(errors).toBeNull();
});
it('missing runtime version', async () => {
const functions = { 'pages/index.ts': { runtime: 'haha' } };
const files = ['pages/index.ts'];

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/gatsby-plugin-vercel-builder",
"version": "1.1.6",
"version": "1.1.7",
"main": "dist/index.js",
"files": [
"dist",
@@ -14,8 +14,8 @@
"build:src": "tsc -p tsconfig.src.json"
},
"dependencies": {
"@vercel/build-utils": "6.2.4",
"@vercel/node": "2.9.5",
"@vercel/build-utils": "6.3.0",
"@vercel/node": "2.9.6",
"@vercel/routing-utils": "2.1.9",
"ajv": "8.12.0",
"esbuild": "0.14.47",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/hydrogen",
"version": "0.0.52",
"version": "0.0.53",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -21,8 +21,8 @@
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "14.18.33",
"@vercel/build-utils": "6.2.4",
"@vercel/static-config": "2.0.12",
"@vercel/build-utils": "6.3.0",
"@vercel/static-config": "2.0.13",
"execa": "3.2.0",
"fs-extra": "11.1.0",
"ts-morph": "12.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.4.5",
"version": "3.5.2",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -45,7 +45,7 @@
"@types/semver": "6.0.0",
"@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/nft": "0.22.5",
"@vercel/routing-utils": "2.1.9",
"async-sema": "3.0.1",

View File

@@ -88,6 +88,8 @@ import {
PseudoLayerResult,
updateRouteSrc,
validateEntrypoint,
getOperationType,
isApiPage,
} from './utils';
export const version = 2;
@@ -1090,10 +1092,14 @@ export const build: BuildV2 = async ({
handler: '___next_launcher.cjs',
runtime: nodeVersion.runtime,
...lambdaOptions,
operationType: 'SSR',
operationType: 'SSR', // always SSR because we're in legacy mode
shouldAddHelpers: false,
shouldAddSourcemapSupport: false,
supportsMultiPayloads: !!process.env.NEXT_PRIVATE_MULTI_PAYLOAD,
framework: {
slug: 'nextjs',
version: nextVersion,
},
});
debug(`Created serverless function for page: "${page}"`);
})
@@ -1122,10 +1128,6 @@ export const build: BuildV2 = async ({
outputDirectory,
appPathRoutesManifest,
});
const isApiPage = (page: string) =>
page
.replace(/\\/g, '/')
.match(/(serverless|server)\/pages\/api(\/|\.js$)/);
const canUsePreviewMode = Object.keys(pages).some(page =>
isApiPage(pages[page].fsPath)
@@ -1594,6 +1596,10 @@ export const build: BuildV2 = async ({
internalPages: [],
});
for (const group of initialApiLambdaGroups) {
group.isApiLambda = true;
}
debug(
JSON.stringify(
{
@@ -1815,7 +1821,12 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType: getOperationType({
prerenderManifest,
pageFileName,
}),
runtime: nodeVersion.runtime,
nextVersion,
...lambdaOptions,
});
} else {
@@ -1834,7 +1845,9 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType: getOperationType({ pageFileName }), // can only be API or SSR
runtime: nodeVersion.runtime,
nextVersion,
...lambdaOptions,
});
}
@@ -2034,6 +2047,11 @@ export const build: BuildV2 = async ({
pageLambdaMap[page] = group.lambdaIdentifier;
}
const operationType = getOperationType({
group,
prerenderManifest,
});
lambdas[group.lambdaIdentifier] =
await createLambdaFromPseudoLayers({
files: {
@@ -2045,7 +2063,9 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType,
runtime: nodeVersion.runtime,
nextVersion,
});
}
)
@@ -2096,6 +2116,8 @@ export const build: BuildV2 = async ({
...Object.entries(prerenderManifest.fallbackRoutes),
...Object.entries(prerenderManifest.blockingFallbackRoutes),
].forEach(([, { dataRouteRegex, dataRoute }]) => {
if (!dataRoute || !dataRouteRegex) return;
dataRoutes.push({
// Next.js provided data route regex
src: dataRouteRegex.replace(

View File

@@ -43,6 +43,7 @@ import {
getMiddlewareBundle,
getFilesMapFromReasons,
UnwrapPromise,
getOperationType,
} from './utils';
import {
nodeFileTrace,
@@ -181,7 +182,10 @@ export async function serverBuild({
}
const pageMatchesApi = (page: string) => {
return page.startsWith('api/') || page === 'api.js';
return (
!appPathRoutesManifest?.[page] &&
(page.startsWith('api/') || page === 'api.js')
);
};
const { i18n } = routesManifest;
@@ -748,6 +752,10 @@ export async function serverBuild({
internalPages,
});
for (const group of apiLambdaGroups) {
group.isApiLambda = true;
}
debug(
JSON.stringify(
{
@@ -856,6 +864,8 @@ export async function serverBuild({
}
}
const operationType = getOperationType({ group, prerenderManifest });
const lambda = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
@@ -869,11 +879,12 @@ export async function serverBuild({
),
'___next_launcher.cjs'
),
operationType,
memory: group.memory,
runtime: nodeVersion.runtime,
maxDuration: group.maxDuration,
isStreaming: group.isStreaming,
cron: group.cron,
nextVersion,
});
for (const page of group.pages) {
@@ -974,6 +985,7 @@ export async function serverBuild({
routesManifest,
isCorrectMiddlewareOrder,
prerenderBypassToken: prerenderManifest.bypassToken || '',
nextVersion,
});
const isNextDataServerResolving =
@@ -1127,8 +1139,19 @@ export async function serverBuild({
// to match prerenders so we can route the same when the
// __rsc__ header is present
const edgeFunctions = middleware.edgeFunctions;
// allow looking up original route from normalized route
const inverseAppPathManifest: Record<string, string> = {};
for (const ogRoute of Object.keys(appPathRoutesManifest)) {
inverseAppPathManifest[appPathRoutesManifest[ogRoute]] = ogRoute;
}
for (let route of Object.values(appPathRoutesManifest)) {
const ogRoute = inverseAppPathManifest[route];
if (ogRoute.endsWith('/route')) {
continue;
}
route = path.posix.join('./', route === '/' ? '/index' : route);
if (lambdas[route]) {
@@ -1141,6 +1164,10 @@ export async function serverBuild({
}
const rscHeader = routesManifest.rsc?.header?.toLowerCase() || '__rsc__';
const rscVaryHeader =
routesManifest?.rsc?.varyHeader ||
'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const completeDynamicRoutes: typeof dynamicRoutes = [];
if (appDir) {
@@ -1413,7 +1440,9 @@ export async function serverBuild({
},
],
dest: path.posix.join('/', entryDirectory, '/index.rsc'),
headers: { vary: rscVaryHeader },
continue: true,
override: true,
},
{
src: `^${path.posix.join(
@@ -1428,7 +1457,9 @@ export async function serverBuild({
},
],
dest: path.posix.join('/', entryDirectory, '/$1.rsc'),
headers: { vary: rscVaryHeader },
continue: true,
override: true,
},
]
: []),

View File

@@ -15,7 +15,6 @@ import {
NodejsLambda,
EdgeFunction,
Images,
Cron,
} from '@vercel/build-utils';
import { NodeFileTraceReasons } from '@vercel/nft';
import type {
@@ -775,6 +774,7 @@ export async function createPseudoLayer(files: {
interface CreateLambdaFromPseudoLayersOptions extends LambdaOptionsWithFiles {
layers: PseudoLayer[];
isStreaming?: boolean;
nextVersion?: string;
}
// measured with 1, 2, 5, 10, and `os.cpus().length || 5`
@@ -785,6 +785,7 @@ export async function createLambdaFromPseudoLayers({
files: baseFiles,
layers,
isStreaming,
nextVersion,
...lambdaOptions
}: CreateLambdaFromPseudoLayersOptions) {
await createLambdaSema.acquire();
@@ -828,6 +829,10 @@ export async function createLambdaFromPseudoLayers({
shouldAddHelpers: false,
shouldAddSourcemapSupport: false,
supportsMultiPayloads: !!process.env.NEXT_PRIVATE_MULTI_PAYLOAD,
framework: {
slug: 'nextjs',
version: nextVersion,
},
});
}
@@ -844,16 +849,18 @@ export type NextPrerenderedRoutes = {
staticRoutes: {
[route: string]: {
initialRevalidate: number | false;
dataRoute: string;
dataRoute: string | null;
srcRoute: string | null;
initialStatus?: number;
initialHeaders?: Record<string, string>;
};
};
blockingFallbackRoutes: {
[route: string]: {
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
@@ -861,16 +868,16 @@ export type NextPrerenderedRoutes = {
[route: string]: {
fallback: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
omittedRoutes: {
[route: string]: {
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
@@ -1063,6 +1070,30 @@ export async function getPrerenderManifest(
previewModeId: string;
};
notFoundRoutes?: string[];
}
| {
version: 4;
routes: {
[route: string]: {
initialRevalidateSeconds: number | false;
srcRoute: string | null;
dataRoute: string | null;
initialStatus?: number;
initialHeaders?: Record<string, string>;
};
};
dynamicRoutes: {
[route: string]: {
routeRegex: string;
fallback: string | false;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
preview: {
previewModeId: string;
};
notFoundRoutes?: string[];
} = JSON.parse(await fs.readFile(pathPrerenderManifest, 'utf8'));
switch (manifest.version) {
@@ -1117,7 +1148,8 @@ export async function getPrerenderManifest(
return ret;
}
case 2:
case 3: {
case 3:
case 4: {
const routes = Object.keys(manifest.routes);
const lazyRoutes = Object.keys(manifest.dynamicRoutes);
@@ -1138,6 +1170,15 @@ export async function getPrerenderManifest(
routes.forEach(route => {
const { initialRevalidateSeconds, dataRoute, srcRoute } =
manifest.routes[route];
let initialStatus: undefined | number;
let initialHeaders: undefined | Record<string, string>;
if (manifest.version === 4) {
initialStatus = manifest.routes[route].initialStatus;
initialHeaders = manifest.routes[route].initialHeaders;
}
ret.staticRoutes[route] = {
initialRevalidate:
initialRevalidateSeconds === false
@@ -1145,6 +1186,8 @@ export async function getPrerenderManifest(
: Math.max(1, initialRevalidateSeconds),
dataRoute,
srcRoute,
initialStatus,
initialHeaders,
};
});
@@ -1305,10 +1348,10 @@ export function addLocaleOrDefault(
export type LambdaGroup = {
pages: string[];
memory?: number;
cron?: Cron;
maxDuration?: number;
isStreaming?: boolean;
isPrerenders?: boolean;
isApiLambda: boolean;
pseudoLayer: PseudoLayer;
pseudoLayerBytes: number;
pseudoLayerUncompressedBytes: number;
@@ -1358,7 +1401,7 @@ export async function getPageLambdaGroups({
const routeName = normalizePage(page.replace(/\.js$/, ''));
const isPrerenderRoute = prerenderRoutes.has(routeName);
let opts: { memory?: number; maxDuration?: number; cron?: Cron } = {};
let opts: { memory?: number; maxDuration?: number } = {};
if (config && config.functions) {
const sourceFile = await getSourceFilePathFromPage({
@@ -1376,8 +1419,7 @@ export async function getPageLambdaGroups({
const matches =
group.maxDuration === opts.maxDuration &&
group.memory === opts.memory &&
group.isPrerenders === isPrerenderRoute &&
!opts.cron; // Functions with a cronjob must be on their own
group.isPrerenders === isPrerenderRoute;
if (matches) {
let newTracedFilesSize = group.pseudoLayerBytes;
@@ -1416,6 +1458,7 @@ export async function getPageLambdaGroups({
pages: [page],
...opts,
isPrerenders: isPrerenderRoute,
isApiLambda: !!isApiPage(page),
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
pseudoLayer: Object.assign({}, initialPseudoLayer.pseudoLayer),
@@ -1647,7 +1690,7 @@ export const onPrerenderRouteInitial = (
const { initialRevalidate, srcRoute, dataRoute } = pr;
const route = srcRoute || routeKey;
const isAppPathRoute = appDir && dataRoute?.endsWith('.rsc');
const isAppPathRoute = appDir && (!dataRoute || dataRoute?.endsWith('.rsc'));
const routeNoLocale = routesManifest?.i18n
? normalizeLocalePath(routeKey, routesManifest.i18n.locales).pathname
@@ -1805,7 +1848,9 @@ export const onPrerenderRoute =
let initialRevalidate: false | number;
let srcRoute: string | null;
let dataRoute: string;
let dataRoute: string | null;
let initialStatus: number | undefined;
let initialHeaders: Record<string, string> | undefined;
if (isFallback || isBlocking) {
const pr = isFallback
@@ -1828,44 +1873,60 @@ export const onPrerenderRoute =
dataRoute = prerenderManifest.omittedRoutes[routeKey].dataRoute;
} else {
const pr = prerenderManifest.staticRoutes[routeKey];
({ initialRevalidate, srcRoute, dataRoute } = pr);
({
initialRevalidate,
srcRoute,
dataRoute,
initialHeaders,
initialStatus,
} = pr);
}
let isAppPathRoute = false;
// TODO: leverage manifest to determine app paths more accurately
if (appDir && srcRoute && dataRoute.endsWith('.rsc')) {
if (appDir && srcRoute && (!dataRoute || dataRoute?.endsWith('.rsc'))) {
isAppPathRoute = true;
}
const isOmittedOrNotFound = isOmitted || isNotFound;
const htmlFsRef =
isBlocking || (isNotFound && !static404Page)
? // Blocking pages do not have an HTML fallback
null
: new FileFsRef({
fsPath: path.join(
isAppPathRoute && !isOmittedOrNotFound && appDir
? appDir
: pagesDir,
isFallback
? // Fallback pages have a special file.
addLocaleOrDefault(
prerenderManifest.fallbackRoutes[routeKey].fallback,
routesManifest,
locale
)
: // Otherwise, the route itself should exist as a static HTML
// file.
`${
isOmittedOrNotFound
? addLocaleOrDefault('/404', routesManifest, locale)
: routeFileNoExt
}.html`
),
});
let htmlFsRef: FileFsRef | null;
if (appDir && !dataRoute && isAppPathRoute && !(isBlocking || isFallback)) {
const contentType = initialHeaders?.['content-type'];
htmlFsRef = new FileFsRef({
fsPath: path.join(appDir, `${routeFileNoExt}.body`),
contentType: contentType || 'text/html;charset=utf-8',
});
} else {
htmlFsRef =
isBlocking || (isNotFound && !static404Page)
? // Blocking pages do not have an HTML fallback
null
: new FileFsRef({
fsPath: path.join(
isAppPathRoute && !isOmittedOrNotFound && appDir
? appDir
: pagesDir,
isFallback
? // Fallback pages have a special file.
addLocaleOrDefault(
prerenderManifest.fallbackRoutes[routeKey].fallback,
routesManifest,
locale
)
: // Otherwise, the route itself should exist as a static HTML
// file.
`${
isOmittedOrNotFound
? addLocaleOrDefault('/404', routesManifest, locale)
: routeFileNoExt
}.html`
),
});
}
const jsonFsRef =
// JSON data does not exist for fallback or blocking pages
isFallback || isBlocking || (isNotFound && !static404Page)
isFallback || isBlocking || (isNotFound && !static404Page) || !dataRoute
? null
: new FileFsRef({
fsPath: path.join(
@@ -1903,16 +1964,20 @@ export const onPrerenderRoute =
);
let lambda: undefined | Lambda;
let outputPathData = path.posix.join(entryDirectory, dataRoute);
let outputPathData: null | string = null;
if (nonDynamicSsg || isFallback || isOmitted) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
// ensure we escape "$" correctly while replacing as "$" is a special
// character, we need to do double escaping as first is for the initial
// replace on the routeFile and then the second on the outputPath
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
);
if (dataRoute) {
outputPathData = path.posix.join(entryDirectory, dataRoute);
if (nonDynamicSsg || isFallback || isOmitted) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
// ensure we escape "$" correctly while replacing as "$" is a special
// character, we need to do double escaping as first is for the initial
// replace on the routeFile and then the second on the outputPath
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
);
}
}
if (isSharedLambdas) {
@@ -1949,7 +2014,7 @@ export const onPrerenderRoute =
if (htmlFsRef == null || jsonFsRef == null) {
throw new NowBuildError({
code: 'NEXT_HTMLFSREF_JSONFSREF',
message: 'invariant: htmlFsRef != null && jsonFsRef != null',
message: `invariant: htmlFsRef != null && jsonFsRef != null ${routeFileNoExt}`,
});
}
@@ -1961,7 +2026,10 @@ export const onPrerenderRoute =
) {
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
if (outputPathData) {
prerenders[outputPathData] = jsonFsRef;
}
}
}
const isNotFoundPreview =
@@ -2023,11 +2091,10 @@ export const onPrerenderRoute =
allowQuery = [];
}
}
const rscVaryHeader =
routesManifest?.rsc?.varyHeader ||
'__rsc__, __next_router_state_tree__, __next_router_prefetch__';
const rscContentTypeHeader =
routesManifest?.rsc?.contentTypeHeader || 'application/octet-stream';
const rscEnabled = !!routesManifest?.rsc;
const rscVaryHeader = routesManifest?.rsc?.varyHeader || 'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const rscContentTypeHeader = routesManifest?.rsc?.contentTypeHeader || 'text/x-component';
prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate,
@@ -2036,27 +2103,8 @@ export const onPrerenderRoute =
fallback: htmlFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
...(isNotFound
? {
initialStatus: 404,
}
: {}),
...(isAppPathRoute
? {
initialHeaders: {
vary: rscVaryHeader,
},
}
: {}),
});
prerenders[outputPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
fallback: jsonFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
initialStatus,
initialHeaders,
...(isNotFound
? {
@@ -2064,16 +2112,42 @@ export const onPrerenderRoute =
}
: {}),
...(isAppPathRoute
...(rscEnabled
? {
initialHeaders: {
'content-type': rscContentTypeHeader,
...initialHeaders,
vary: rscVaryHeader,
},
}
: {}),
});
if (outputPathData) {
prerenders[outputPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
fallback: jsonFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
...(isNotFound
? {
initialStatus: 404,
}
: {}),
...(rscEnabled
? {
initialHeaders: {
'content-type': rscContentTypeHeader,
vary: rscVaryHeader,
},
}
: {}),
});
}
++prerenderGroup;
if (routesManifest?.i18n && isBlocking) {
@@ -2087,29 +2161,30 @@ export const onPrerenderRoute =
path.posix.join(entryDirectory, localeRouteFileNoExt),
isServerMode
);
const localeOutputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${localeRouteFileNoExt}${
localeRouteFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
const origPrerenderPage = prerenders[outputPathPage];
const origPrerenderData = prerenders[outputPathData];
prerenders[localeOutputPathPage] = {
...origPrerenderPage,
group: prerenderGroup,
} as Prerender;
prerenders[localeOutputPathData] = {
...origPrerenderData,
group: prerenderGroup,
} as Prerender;
if (outputPathData) {
const localeOutputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${localeRouteFileNoExt}${
localeRouteFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
const origPrerenderData = prerenders[outputPathData];
prerenders[localeOutputPathData] = {
...origPrerenderData,
group: prerenderGroup,
} as Prerender;
}
++prerenderGroup;
}
}
@@ -2312,12 +2387,12 @@ interface EdgeFunctionMatcher {
}
export async function getMiddlewareBundle({
config = {},
entryPath,
outputDirectory,
routesManifest,
isCorrectMiddlewareOrder,
prerenderBypassToken,
nextVersion,
}: {
config: Config;
entryPath: string;
@@ -2325,6 +2400,7 @@ export async function getMiddlewareBundle({
prerenderBypassToken: string;
routesManifest: RoutesManifest;
isCorrectMiddlewareOrder: boolean;
nextVersion: string;
}): Promise<{
staticRoutes: Route[];
dynamicRouteMap: Map<string, RouteWithSrc>;
@@ -2375,21 +2451,6 @@ export async function getMiddlewareBundle({
edgeFunction.wasm
);
const edgeFunctionOptions: { cron?: Cron } = {};
if (config.functions) {
const sourceFile = await getSourceFilePathFromPage({
workPath: entryPath,
page: `${edgeFunction.page}.js`,
});
const opts = await getLambdaOptionsFromFunction({
sourceFile,
config,
});
edgeFunctionOptions.cron = opts.cron;
}
return {
type,
page: edgeFunction.page,
@@ -2434,7 +2495,6 @@ export async function getMiddlewareBundle({
);
return new EdgeFunction({
...edgeFunctionOptions,
deploymentTarget: 'v8-worker',
name: edgeFunction.name,
files: {
@@ -2462,6 +2522,10 @@ export async function getMiddlewareBundle({
path: `assets/${name}`,
};
}),
framework: {
slug: 'nextjs',
version: nextVersion,
},
});
})(),
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
@@ -2497,9 +2561,13 @@ export async function getMiddlewareBundle({
// app/index/page -> index/index
if (shortPath.startsWith('pages/')) {
shortPath = shortPath.replace(/^pages\//, '');
} else if (shortPath.startsWith('app/') && shortPath.endsWith('/page')) {
} else if (
shortPath.startsWith('app/') &&
(shortPath.endsWith('/page') || shortPath.endsWith('/route'))
) {
shortPath =
shortPath.replace(/^app\//, '').replace(/(^|\/)page$/, '') || 'index';
shortPath.replace(/^app\//, '').replace(/(^|\/)(page|route)$/, '') ||
'index';
}
if (routesManifest?.basePath) {
@@ -2685,3 +2753,49 @@ function transformSourceMap(
return { ...sourcemap, sources };
}
interface LambdaGroupTypeInterface {
isApiLambda: boolean;
isPrerenders?: boolean;
}
export function getOperationType({
group,
prerenderManifest,
pageFileName,
}: {
group?: LambdaGroupTypeInterface;
prerenderManifest?: NextPrerenderedRoutes;
pageFileName?: string;
}) {
if (group?.isApiLambda || isApiPage(pageFileName)) {
return 'API';
}
if (group?.isPrerenders) {
return 'ISR';
}
if (pageFileName && prerenderManifest) {
const { blockingFallbackRoutes = {}, fallbackRoutes = {} } =
prerenderManifest;
if (
pageFileName in blockingFallbackRoutes ||
pageFileName in fallbackRoutes
) {
return 'ISR';
}
}
return 'SSR';
}
export function isApiPage(page: string | undefined) {
if (!page) {
return false;
}
return page
.replace(/\\/g, '/')
.match(/(serverless|server)\/pages\/api(\/|\.js$)/);
}

View File

@@ -0,0 +1,21 @@
import Link from 'next/link';
const paths = ['/', '/shop', '/product', '/who-we-are', '/about', '/contact'];
export default function Page({ params }) {
return (
<>
<p>variant: {params.variant}</p>
<p>slug: {params.rest?.join('/')}</p>
<ul>
{paths.map(path => {
return (
<li key={path}>
<Link href={path}>to {path}</Link>
</li>
);
})}
</ul>
</>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server';
export function middleware(request) {
request.nextUrl.pathname = `/no-variant${request.nextUrl.pathname}`;
return NextResponse.rewrite(request.nextUrl);
}
// See "Matching Paths" below to learn more
export const config = {
matcher: ['/', '/shop', '/product', '/who-we-are', '/about', '/contact'],
};

View File

@@ -0,0 +1,14 @@
module.exports = {
experimental: {
appDir: true,
runtime: 'nodejs',
},
rewrites: async () => {
return [
{
source: '/rewritten-to-dashboard',
destination: '/dashboard',
},
];
},
};

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "experimental",
"react-dom": "experimental"
}
}

View File

@@ -0,0 +1,3 @@
export default function handler(req, res) {
return res.json({ hello: 'world' });
}

View File

@@ -0,0 +1,7 @@
export default function Page(props) {
return (
<>
<p>hello from pages/blog/[slug]</p>
</>
);
}

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1,82 @@
{
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "index"
},
{
"path": "/",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
},
{
"path": "/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "shop.rsc"
},
{
"path": "/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
},
{
"path": "/no-variant/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "shop.rsc"
},
{
"path": "/no-variant/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
}
]
}

View File

@@ -0,0 +1,7 @@
export const runtime = 'experimental-edge';
export const GET = req => {
// use query to trigger dynamic usage
console.log('query', Object.fromEntries(req.nextUrl.searchParams));
return new Response('hello world');
};

View File

@@ -32,6 +32,10 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
expect(buildResult.output['dashboard/changelog']).toBeDefined();
expect(buildResult.output['dashboard/deployments/[id]']).toBeDefined();
expect(buildResult.output['edge-route-handler']).toBeDefined();
expect(buildResult.output['edge-route-handler'].type).toBe('EdgeFunction');
expect(buildResult.output['edge-route-handler.rsc']).not.toBeDefined();
// prefixed static generation output with `/app` under dist server files
expect(buildResult.output['dashboard'].type).toBe('Prerender');
expect(buildResult.output['dashboard'].fallback.fsPath).toMatch(
@@ -134,41 +138,70 @@ it('should build using server build', async () => {
expect(output['index'].allowQuery).toBe(undefined);
expect(output['index'].memory).toBe(512);
expect(output['index'].maxDuration).toBe(5);
expect(output['index'].operationType).toBe('SSR');
expect(output['another'].type).toBe('Lambda');
expect(output['another'].memory).toBe(512);
expect(output['another'].maxDuration).toBe(5);
expect(output['another'].allowQuery).toBe(undefined);
expect(output['another'].operationType).toBe('SSR');
expect(output['dynamic/[slug]'].type).toBe('Lambda');
expect(output['dynamic/[slug]'].memory).toBe(undefined);
expect(output['dynamic/[slug]'].maxDuration).toBe(5);
expect(output['dynamic/[slug]'].operationType).toBe('SSR');
expect(output['fallback/[slug]'].type).toBe('Prerender');
expect(output['fallback/[slug]'].allowQuery).toEqual(['slug']);
expect(output['fallback/[slug]'].lambda.operationType).toBe('ISR');
expect(output['_next/data/testing-build-id/fallback/[slug].json'].type).toBe(
'Prerender'
);
expect(
output['_next/data/testing-build-id/fallback/[slug].json'].allowQuery
).toEqual(['slug']);
expect(
output['_next/data/testing-build-id/fallback/[slug].json'].lambda
.operationType
).toBe('ISR');
expect(output['fallback/first'].type).toBe('Prerender');
expect(output['fallback/first'].allowQuery).toEqual([]);
expect(output['fallback/first'].lambda.operationType).toBe('ISR');
expect(output['_next/data/testing-build-id/fallback/first.json'].type).toBe(
'Prerender'
);
expect(
output['_next/data/testing-build-id/fallback/first.json'].allowQuery
).toEqual([]);
expect(
output['_next/data/testing-build-id/fallback/first.json'].lambda
.operationType
).toBe('ISR');
expect(output['api'].type).toBe('Lambda');
expect(output['api'].allowQuery).toBe(undefined);
expect(output['api'].memory).toBe(128);
expect(output['api'].maxDuration).toBe(5);
expect(output['api'].operationType).toBe('API');
expect(output['api/another'].type).toBe('Lambda');
expect(output['api/another'].allowQuery).toBe(undefined);
expect(output['api/another'].operationType).toBe('API');
expect(output['api/blog/[slug]'].type).toBe('Lambda');
expect(output['api/blog/[slug]'].allowQuery).toBe(undefined);
expect(output['api/blog/[slug]'].operationType).toBe('API');
expect(output['static'].type).toBe('FileFsRef');
expect(output['static'].allowQuery).toBe(undefined);
expect(output['static'].operationType).toBe(undefined);
expect(output['ssg'].type).toBe('Prerender');
expect(output['ssg'].allowQuery).toEqual([]);
expect(output['ssg'].lambda.operationType).toBe('ISR');
expect(output['index'] === output['another']).toBe(true);
expect(output['dynamic/[slug]'] !== output['fallback/[slug]'].lambda).toBe(

View File

@@ -1,44 +0,0 @@
const path = require('node:path');
const fs = require('fs-extra');
const { build } = require('../../dist');
function getFixture(name) {
return path.join(__dirname, 'fixtures', name);
}
const initialCorepackValue = process.env.COREPACK_ENABLE_STRICT;
beforeEach(() => {
process.env.COREPACK_ENABLE_STRICT = '0';
});
afterEach(() => {
process.env.COREPACK_ENABLE_STRICT = initialCorepackValue;
});
it('should include cron property from config', async () => {
const cwd = getFixture('03-with-api-routes');
await fs.remove(path.join(cwd, '.next'));
const result = await build({
workPath: cwd,
repoRootPath: cwd,
entrypoint: 'package.json',
config: {
functions: {
'pages/api/edge.js': {
cron: '* * * * *',
},
'pages/api/serverless.js': {
cron: '* * * * *',
},
},
},
meta: {
skipDownload: true,
},
});
expect(result.output['api/serverless']).toHaveProperty('cron', '* * * * *');
expect(result.output['api/edge']).toHaveProperty('cron', '* * * * *');
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "2.9.5",
"version": "2.9.6",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -31,9 +31,9 @@
"dependencies": {
"@edge-runtime/vm": "2.0.0",
"@types/node": "14.18.33",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/node-bridge": "3.1.11",
"@vercel/static-config": "2.0.12",
"@vercel/static-config": "2.0.13",
"edge-runtime": "2.0.0",
"esbuild": "0.14.47",
"exit-hook": "2.2.1",

View File

@@ -424,8 +424,6 @@ export const build: BuildV3 = async ({
isEdgeFunction = isEdgeRuntime(staticConfig.runtime);
}
const cron = staticConfig?.cron;
debug('Tracing input files...');
const traceTime = Date.now();
const { preparedFiles, shouldAddSourcemapSupport } = await compile(
@@ -475,7 +473,6 @@ export const build: BuildV3 = async ({
// TODO: remove - these two properties should not be required
name: outputPath,
deploymentTarget: 'v8-worker',
cron,
});
} else {
// "nodejs" runtime is the default
@@ -494,7 +491,6 @@ export const build: BuildV3 = async ({
shouldAddSourcemapSupport,
awsLambdaHandler,
experimentalResponseStreaming,
cron,
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/python",
"version": "3.1.48",
"version": "3.1.49",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -23,7 +23,7 @@
"@types/execa": "^0.9.0",
"@types/jest": "27.4.1",
"@types/node": "14.18.33",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/ncc": "0.24.0",
"execa": "^1.0.0",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/redwood",
"version": "1.1.4",
"version": "1.1.5",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs",
@@ -27,7 +27,7 @@
"@types/aws-lambda": "8.10.19",
"@types/node": "14.18.33",
"@types/semver": "6.0.0",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"execa": "3.2.0",
"fs-extra": "11.1.0",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/remix",
"version": "1.3.1",
"version": "1.3.5",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -11,8 +11,9 @@
},
"scripts": {
"build": "node build.js",
"test-e2e": "pnpm test test/integration.test.ts",
"test": "jest --env node --verbose --bail --runInBand"
"test": "jest --env node --verbose --bail --runInBand",
"test-unit": "pnpm test test/unit.*test.*",
"test-e2e": "pnpm test test/integration.test.ts"
},
"files": [
"dist",
@@ -22,14 +23,14 @@
"dependencies": {
"@remix-run/dev": "1.12.0",
"@vercel/nft": "0.22.5",
"@vercel/static-config": "2.0.12",
"@vercel/static-config": "2.0.13",
"path-to-regexp": "6.2.1",
"ts-morph": "12.0.0"
},
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "14.18.33",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"typescript": "4.9.4"
}
}

View File

@@ -4,7 +4,11 @@ import {
Headers as NodeHeaders,
Request as NodeRequest,
writeReadableStreamToWritable,
installGlobals,
} from '@remix-run/node';
installGlobals();
import build from './index.js';
const handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV);

View File

@@ -1,7 +1,6 @@
import { Project } from 'ts-morph';
import { promises as fs } from 'fs';
import { basename, dirname, extname, join, relative, sep } from 'path';
import { pathToRegexp, Key } from 'path-to-regexp';
import {
debug,
download,
@@ -30,7 +29,12 @@ import type {
BuildResultV2Typical,
} from '@vercel/build-utils';
import type { ConfigRoute } from '@remix-run/dev/dist/config/routes';
import { findConfig } from './utils';
import {
findConfig,
getPathFromRoute,
getRegExpFromPath,
isLayoutRoute,
} from './utils';
const _require: typeof require = eval('require');
@@ -173,12 +177,13 @@ module.exports = config;`;
}
}
const { serverBuildPath, routes: remixRoutes } = remixConfig;
const { serverBuildPath } = remixConfig;
const remixRoutes = Object.values(remixConfig.routes);
// Figure out which pages should be edge functions
const edgePages = new Set<ConfigRoute>();
const project = new Project();
for (const route of Object.values(remixRoutes)) {
for (const route of remixRoutes) {
const routePath = join(remixConfig.appDirectory, route.file);
const staticConfig = getConfig(project, routePath);
const isEdge =
@@ -228,27 +233,11 @@ module.exports = config;`;
},
];
for (const route of Object.values(remixRoutes)) {
for (const route of remixRoutes) {
// Layout routes don't get a function / route added
const isLayoutRoute = Object.values(remixRoutes).some(
r => r.parentId === route.id
);
if (isLayoutRoute) continue;
// Build up the full request path
let currentRoute: ConfigRoute | undefined = route;
const pathParts: string[] = [];
do {
if (currentRoute.index) pathParts.push('index');
if (currentRoute.path) pathParts.push(currentRoute.path);
if (currentRoute.parentId) {
currentRoute = remixRoutes[currentRoute.parentId];
} else {
currentRoute = undefined;
}
} while (currentRoute);
const path = join(...pathParts.reverse());
if (isLayoutRoute(route.id, remixRoutes)) continue;
const path = getPathFromRoute(route, remixConfig.routes);
const isEdge = edgePages.has(route);
const fn =
isEdge && edgeFunction
@@ -264,11 +253,8 @@ module.exports = config;`;
output[path] = fn;
// If this is a dynamic route then add a Vercel route
const keys: Key[] = [];
// Replace "/*" at the end to handle "splat routes"
const rePath = `/${path.replace(/\/\*$/, '/:params+')}`;
const re = pathToRegexp(rePath, keys);
if (keys.length > 0) {
const re = getRegExpFromPath(path);
if (re) {
routes.push({
src: re.source,
dest: path,

View File

@@ -1,5 +1,10 @@
import { existsSync } from 'fs';
import { join } from 'path';
import { existsSync } from 'fs';
import { pathToRegexp, Key } from 'path-to-regexp';
import type {
ConfigRoute,
RouteManifest,
} from '@remix-run/dev/dist/config/routes';
const configExts = ['.js', '.cjs', '.mjs'];
@@ -12,3 +17,39 @@ export function findConfig(dir: string, basename: string): string | undefined {
return undefined;
}
export function isLayoutRoute(
routeId: string,
routes: Pick<ConfigRoute, 'id' | 'parentId'>[]
): boolean {
return routes.some(r => r.parentId === routeId);
}
export function getPathFromRoute(
route: ConfigRoute,
routes: RouteManifest
): string {
let currentRoute: ConfigRoute | undefined = route;
const pathParts: string[] = [];
do {
if (currentRoute.index) pathParts.push('index');
if (currentRoute.path) pathParts.push(currentRoute.path);
if (currentRoute.parentId) {
currentRoute = routes[currentRoute.parentId];
} else {
currentRoute = undefined;
}
} while (currentRoute);
const path = pathParts.reverse().join('/');
return path;
}
export function getRegExpFromPath(path: string): RegExp | false {
const keys: Key[] = [];
// Replace "/*" at the end to handle "splat routes"
const splatPath = '/:params+';
const rePath =
path === '*' ? splatPath : `/${path.replace(/\/\*$/, splatPath)}`;
const re = pathToRegexp(rePath, keys);
return keys.length > 0 ? re : false;
}

View File

@@ -0,0 +1,13 @@
import { json } from '@remix-run/server-runtime';
import { useLoaderData } from '@remix-run/react';
import type { LoaderArgs } from '@remix-run/server-runtime';
export const loader = ({ request }: LoaderArgs) => {
const instanceOfRequest = request instanceof Request;
return json({ instanceOfRequest });
};
export default function InstanceOf() {
const data = useLoaderData<typeof loader>();
return <div>{`InstanceOfRequest: ${data.instanceOfRequest}`}</div>;
}

View File

@@ -16,6 +16,7 @@
{ "path": "/nested", "mustContain": "Nested index page" },
{ "path": "/nested/another", "mustContain": "Nested another page" },
{ "path": "/nested/index", "mustContain": "Not Found" },
{ "path": "/asdf", "mustContain": "Not Found" }
{ "path": "/asdf", "mustContain": "Not Found" },
{ "path": "/instanceof", "mustContain": "InstanceOfRequest: true" }
]
}

View File

@@ -0,0 +1,6 @@
import { useParams } from "@remix-run/react";
export default function CatchAll() {
const params = useParams();
return <div>{params['*']}</div>;
}

View File

@@ -15,7 +15,7 @@
{ "path": "/b", "mustContain": "B page" },
{ "path": "/nested", "mustContain": "Nested index page" },
{ "path": "/nested/another", "mustContain": "Nested another page" },
{ "path": "/nested/index", "mustContain": "Not Found" },
{ "path": "/asdf", "mustContain": "Not Found" }
{ "path": "/nested/index", "mustContain": "<div>nested/index</div>" },
{ "path": "/asdf", "mustContain": "<div>asdf</div>" }
]
}

View File

@@ -0,0 +1,17 @@
import { join } from 'path';
import { findConfig } from '../src/utils';
const fixture = (name: string) => join(__dirname, 'fixtures', name);
describe('findConfig()', () => {
it.each([
{ name: '01-remix-basics', config: 'remix.config.js' },
{ name: '02-remix-basics-mjs', config: 'remix.config.mjs' },
{ name: '03-with-pnpm', config: 'remix.config.js' },
{ name: '04-with-npm9-linked', config: 'remix.config.js' },
])('should find `$config` from "$name"', ({ name, config }) => {
const dir = fixture(name);
const resolved = findConfig(dir, 'remix.config');
expect(resolved).toEqual(join(dir, config));
});
});

View File

@@ -0,0 +1,73 @@
import { getPathFromRoute } from '../src/utils';
import type { RouteManifest } from '@remix-run/dev/dist/config/routes';
describe('getPathFromRoute()', () => {
const routes: RouteManifest = {
root: { path: '', id: 'root', file: 'root.tsx' },
'routes/$foo.$bar.$baz': {
path: ':foo/:bar/:baz',
id: 'routes/$foo.$bar.$baz',
parentId: 'root',
file: 'routes/$foo.$bar.$baz.tsx',
},
'routes/api.hello': {
path: 'api/hello',
id: 'routes/api.hello',
parentId: 'root',
file: 'routes/api.hello.tsx',
},
'routes/projects': {
path: 'projects',
id: 'routes/projects',
parentId: 'root',
file: 'routes/projects.tsx',
},
'routes/projects/index': {
path: undefined,
index: true,
id: 'routes/projects/indexx',
parentId: 'routes/projects',
file: 'routes/projects/indexx.tsx',
},
'routes/projects/$': {
path: '*',
id: 'routes/projects/$',
parentId: 'routes/projects',
file: 'routes/projects/$.tsx',
},
'routes/index': {
path: undefined,
index: true,
id: 'routes/index',
parentId: 'root',
file: 'routes/index.tsx',
},
'routes/node': {
path: 'node',
id: 'routes/node',
parentId: 'root',
file: 'routes/node.tsx',
},
'routes/$': {
path: '*',
id: 'routes/$',
parentId: 'root',
file: 'routes/$.tsx',
},
};
it.each([
{ id: 'root', expected: '' },
{ id: 'routes/index', expected: 'index' },
{ id: 'routes/api.hello', expected: 'api/hello' },
{ id: 'routes/projects', expected: 'projects' },
{ id: 'routes/projects/index', expected: 'projects/index' },
{ id: 'routes/projects/$', expected: 'projects/*' },
{ id: 'routes/$foo.$bar.$baz', expected: ':foo/:bar/:baz' },
{ id: 'routes/node', expected: 'node' },
{ id: 'routes/$', expected: '*' },
])('should return `$expected` for "$id" route', ({ id, expected }) => {
const route = routes[id];
expect(getPathFromRoute(route, routes)).toEqual(expected);
});
});

View File

@@ -0,0 +1,124 @@
import { getRegExpFromPath } from '../src/utils';
describe('getRegExpFromPath()', () => {
describe('paths without parameters', () => {
it.each([{ path: 'index' }, { path: 'api/hello' }, { path: 'projects' }])(
'should return `false` for "$path"',
({ path }) => {
expect(getRegExpFromPath(path)).toEqual(false);
}
);
});
describe.each([
{
path: '*',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: true,
},
{
url: '/projects/foo',
expected: true,
},
{
url: '/projects/another',
expected: true,
},
{
url: '/to/infinity/and/beyond',
expected: true,
},
],
},
{
path: 'projects/*',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: false,
},
{
url: '/projects/foo',
expected: true,
},
{
url: '/projects/another',
expected: true,
},
],
},
{
path: ':foo',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: true,
},
{
url: '/projects/foo',
expected: false,
},
{
url: '/projects/another',
expected: false,
},
],
},
{
path: 'blog/:id/edit',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: false,
},
{
url: '/blog/123/edit',
expected: true,
},
{
url: '/blog/456/edit',
expected: true,
},
{
url: '/blog/123/456/edit',
expected: false,
},
{
url: '/blog/123/another',
expected: false,
},
],
},
])('with path "$path"', ({ path, urls }) => {
const re = getRegExpFromPath(path) as RegExp;
it('should return RegExp', () => {
expect(re).toBeInstanceOf(RegExp);
});
it.each(urls)(
'should match URL "$url" - $expected',
({ url, expected }) => {
expect(re.test(url)).toEqual(expected);
}
);
});
});

View File

@@ -0,0 +1,21 @@
import { isLayoutRoute } from '../src/utils';
describe('isLayoutRoute()', () => {
const routes = [
{ id: 'root' },
{ id: 'routes/auth', parentId: 'root' },
{ id: 'routes/login', parentId: 'routes/auth' },
{ id: 'routes/logout', parentId: 'routes/auth' },
{ id: 'routes/index', parentId: 'root' },
];
it.each([
{ id: 'root', expected: true },
{ id: 'routes/auth', expected: true },
{ id: 'routes/index', expected: false },
{ id: 'routes/login', expected: false },
{ id: 'routes/logout', expected: false },
])('should return `$expected` for "$id" route', ({ id, expected }) => {
expect(isLayoutRoute(id, routes)).toEqual(expected);
});
});

View File

@@ -1,7 +1,7 @@
{
"name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.3.64",
"version": "1.3.66",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
@@ -22,7 +22,7 @@
"devDependencies": {
"@types/fs-extra": "8.0.0",
"@types/semver": "6.0.0",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/ncc": "0.24.0",
"execa": "2.0.4",
"fs-extra": "^7.0.1",

View File

@@ -1,5 +1,8 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
"probes": [
{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "method": "HEAD", "status": 200 }
]
}

View File

@@ -73,7 +73,7 @@ def webrick_handler(httpMethod, path, body, headers)
{
:statusCode => res.code.to_i,
:headers => res_headers,
:body => res.body,
:body => res.body.nil? ? "" : res.body,
}
end

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "1.3.8",
"version": "1.3.10",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@vercel/gatsby-plugin-vercel-analytics": "1.0.7",
"@vercel/gatsby-plugin-vercel-builder": "1.1.6"
"@vercel/gatsby-plugin-vercel-builder": "1.1.7"
},
"devDependencies": {
"@types/aws-lambda": "8.10.64",
@@ -42,12 +42,12 @@
"@types/node-fetch": "2.5.4",
"@types/promise-timeout": "1.3.0",
"@types/semver": "7.3.13",
"@vercel/build-utils": "6.2.4",
"@vercel/build-utils": "6.3.0",
"@vercel/frameworks": "1.3.1",
"@vercel/fs-detectors": "3.7.13",
"@vercel/fs-detectors": "3.8.0",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.1.9",
"@vercel/static-config": "2.0.12",
"@vercel/static-config": "2.0.13",
"execa": "3.2.0",
"fs-extra": "10.0.0",
"get-port": "5.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-config",
"version": "2.0.12",
"version": "2.0.13",
"license": "MIT",
"main": "./dist/index",
"repository": {

View File

@@ -14,7 +14,6 @@ export const BaseFunctionConfigSchema = {
type: 'object',
properties: {
runtime: { type: 'string' },
cron: { type: 'string' },
memory: { type: 'number' },
maxDuration: { type: 'number' },
regions: {

62
pnpm-lock.yaml generated
View File

@@ -204,23 +204,23 @@ importers:
'@types/which': 1.3.2
'@types/write-json-file': 2.2.1
'@types/yauzl-promise': 2.1.0
'@vercel/build-utils': 6.2.4
'@vercel/client': 12.3.10
'@vercel/build-utils': 6.3.0
'@vercel/client': 12.4.0
'@vercel/error-utils': 1.0.8
'@vercel/frameworks': 1.3.1
'@vercel/fs-detectors': 3.7.13
'@vercel/fs-detectors': 3.8.0
'@vercel/fun': 1.0.4
'@vercel/go': 2.3.6
'@vercel/hydrogen': 0.0.52
'@vercel/go': 2.3.7
'@vercel/hydrogen': 0.0.53
'@vercel/ncc': 0.24.0
'@vercel/next': 3.4.5
'@vercel/node': 2.9.5
'@vercel/python': 3.1.48
'@vercel/redwood': 1.1.4
'@vercel/remix': 1.3.1
'@vercel/next': 3.5.2
'@vercel/node': 2.9.6
'@vercel/python': 3.1.49
'@vercel/redwood': 1.1.5
'@vercel/remix': 1.3.5
'@vercel/routing-utils': 2.1.9
'@vercel/ruby': 1.3.64
'@vercel/static-build': 1.3.8
'@vercel/ruby': 1.3.66
'@vercel/static-build': 1.3.10
'@zeit/source-map-support': 0.6.2
ajv: 6.12.2
alpha-sort: 2.0.1
@@ -446,7 +446,7 @@ importers:
'@types/node-fetch': 2.5.4
'@types/recursive-readdir': 2.2.0
'@types/tar-fs': 1.16.1
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/routing-utils': 2.1.9
'@zeit/fetch': 5.2.0
async-retry: 1.2.3
@@ -551,7 +551,7 @@ importers:
'@types/minimatch': 3.0.5
'@types/node': 14.18.33
'@types/semver': 7.3.10
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/error-utils': 1.0.8
'@vercel/frameworks': 1.3.1
'@vercel/routing-utils': 2.1.9
@@ -599,8 +599,8 @@ importers:
'@types/fs-extra': 11.0.1
'@types/node': 14.18.33
'@types/react': 18.0.26
'@vercel/build-utils': 6.2.4
'@vercel/node': 2.9.5
'@vercel/build-utils': 6.3.0
'@vercel/node': 2.9.6
'@vercel/routing-utils': 2.1.9
ajv: 8.12.0
esbuild: 0.14.47
@@ -634,7 +634,7 @@ importers:
'@types/node': 14.18.33
'@types/node-fetch': ^2.3.0
'@types/tar': ^4.0.0
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/ncc': 0.24.0
async-retry: 1.3.1
execa: ^1.0.0
@@ -666,8 +666,8 @@ importers:
specifiers:
'@types/jest': 27.5.1
'@types/node': 14.18.33
'@vercel/build-utils': 6.2.4
'@vercel/static-config': 2.0.12
'@vercel/build-utils': 6.3.0
'@vercel/static-config': 2.0.13
execa: 3.2.0
fs-extra: 11.1.0
ts-morph: 12.0.0
@@ -697,7 +697,7 @@ importers:
'@types/semver': 6.0.0
'@types/text-table': 0.2.1
'@types/webpack-sources': 3.2.0
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/nft': 0.22.5
'@vercel/routing-utils': 2.1.9
async-sema: 3.0.1
@@ -776,11 +776,11 @@ importers:
'@types/node': 14.18.33
'@types/node-fetch': ^2.6.1
'@types/test-listen': 1.1.0
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/ncc': 0.24.0
'@vercel/nft': 0.22.5
'@vercel/node-bridge': 3.1.11
'@vercel/static-config': 2.0.12
'@vercel/static-config': 2.0.13
content-type: 1.0.4
cookie: 0.4.0
edge-runtime: 2.0.0
@@ -860,7 +860,7 @@ importers:
'@types/execa': ^0.9.0
'@types/jest': 27.4.1
'@types/node': 14.18.33
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/ncc': 0.24.0
execa: ^1.0.0
typescript: 4.3.4
@@ -878,7 +878,7 @@ importers:
'@types/aws-lambda': 8.10.19
'@types/node': 14.18.33
'@types/semver': 6.0.0
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/nft': 0.22.5
'@vercel/routing-utils': 2.1.9
execa: 3.2.0
@@ -903,9 +903,9 @@ importers:
'@remix-run/dev': 1.12.0
'@types/jest': 27.5.1
'@types/node': 14.18.33
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/nft': 0.22.5
'@vercel/static-config': 2.0.12
'@vercel/static-config': 2.0.13
path-to-regexp: 6.2.1
ts-morph: 12.0.0
typescript: 4.9.4
@@ -960,7 +960,7 @@ importers:
specifiers:
'@types/fs-extra': 8.0.0
'@types/semver': 6.0.0
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/ncc': 0.24.0
execa: 2.0.4
fs-extra: ^7.0.1
@@ -987,14 +987,14 @@ importers:
'@types/node-fetch': 2.5.4
'@types/promise-timeout': 1.3.0
'@types/semver': 7.3.13
'@vercel/build-utils': 6.2.4
'@vercel/build-utils': 6.3.0
'@vercel/frameworks': 1.3.1
'@vercel/fs-detectors': 3.7.13
'@vercel/fs-detectors': 3.8.0
'@vercel/gatsby-plugin-vercel-analytics': 1.0.7
'@vercel/gatsby-plugin-vercel-builder': 1.1.6
'@vercel/gatsby-plugin-vercel-builder': 1.1.7
'@vercel/ncc': 0.24.0
'@vercel/routing-utils': 2.1.9
'@vercel/static-config': 2.0.12
'@vercel/static-config': 2.0.13
execa: 3.2.0
fs-extra: 10.0.0
get-port: 5.0.0

View File

@@ -384,7 +384,7 @@ async function fetchDeploymentUrl(url, opts) {
for (let i = 0; i < 50; i += 1) {
const resp = await fetch(url, opts);
const text = await resp.text();
if (text && !text.includes('Join Free')) {
if (typeof text !== 'undefined' && !text.includes('Join Free')) {
return { resp, text };
}