mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
Compare commits
21 Commits
@vercel/no
...
@vercel/fu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6476f4f786 | ||
|
|
a5ea04154b | ||
|
|
5a532a5b94 | ||
|
|
c1d8522950 | ||
|
|
50fc27ba57 | ||
|
|
3101d2497f | ||
|
|
4b0716b556 | ||
|
|
124846a3e6 | ||
|
|
30cd0ec898 | ||
|
|
dc974b6797 | ||
|
|
58c6755e0c | ||
|
|
f18575fcd0 | ||
|
|
d6412be6bc | ||
|
|
52e435aa5d | ||
|
|
303256343a | ||
|
|
88f3a816d9 | ||
|
|
3420ba0153 | ||
|
|
3fb97d1d27 | ||
|
|
f11c7024c4 | ||
|
|
5eb4cc6f5d | ||
|
|
b1adaf76ec |
@@ -37,7 +37,7 @@ You can use the `dev` script to run local changes as if you were invoking Vercel
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via [GitHub Discussions](https://github.com/vercel/vercel/discussions/new) with the owners of this repository before submitting a Pull Request.
|
||||
|
||||
Please read our [Code of Conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
|
||||
Please read our [Code of Conduct](./.github/CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
|
||||
|
||||
### Local development
|
||||
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# vercel
|
||||
|
||||
## 34.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5a532a5b9`](https://github.com/vercel/vercel/commit/5a532a5b948994ba04783ac560357eed9f94a3f3), [`50fc27ba5`](https://github.com/vercel/vercel/commit/50fc27ba5773870956300bbbaffbe387d549bc12), [`c1d852295`](https://github.com/vercel/vercel/commit/c1d85229509dd319a1f11beb940a759113564d33), [`a5ea04154`](https://github.com/vercel/vercel/commit/a5ea04154ba26ee4e635d8953aa4f0d9d82d3a96)]:
|
||||
- @vercel/next@4.2.8
|
||||
- @vercel/node@3.1.0
|
||||
|
||||
## 34.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`52e435aa5`](https://github.com/vercel/vercel/commit/52e435aa5d7b4014d19477969ad5cbfbe94aa76f), [`124846a3e`](https://github.com/vercel/vercel/commit/124846a3e65a3bf1ae82327fd4ba2b132674fb39), [`dc974b679`](https://github.com/vercel/vercel/commit/dc974b6797de0b6e90373c92e1f2bbdafcfc6687), [`58c6755e0`](https://github.com/vercel/vercel/commit/58c6755e0c12cae2ce55978b7bf8722133151196)]:
|
||||
- @vercel/next@4.2.7
|
||||
- @vercel/static-build@2.5.3
|
||||
|
||||
## 34.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3420ba015`](https://github.com/vercel/vercel/commit/3420ba0153dcabffef7114ba2361fb0f3c43a7b3)]:
|
||||
- @vercel/next@4.2.6
|
||||
|
||||
## 34.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b1adaf76e`](https://github.com/vercel/vercel/commit/b1adaf76ec17d1bbfe30a2bf65405bd886fa9bcf), [`3fb97d1d2`](https://github.com/vercel/vercel/commit/3fb97d1d270e835ce34a687bd234ea53dfe446a2)]:
|
||||
- @vercel/next@4.2.5
|
||||
- @vercel/static-build@2.5.2
|
||||
|
||||
## 34.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "34.1.5",
|
||||
"version": "34.1.9",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -36,13 +36,13 @@
|
||||
"@vercel/fun": "1.1.0",
|
||||
"@vercel/go": "3.1.1",
|
||||
"@vercel/hydrogen": "1.0.2",
|
||||
"@vercel/next": "4.2.4",
|
||||
"@vercel/node": "3.0.28",
|
||||
"@vercel/next": "4.2.8",
|
||||
"@vercel/node": "3.1.0",
|
||||
"@vercel/python": "4.2.0",
|
||||
"@vercel/redwood": "2.0.8",
|
||||
"@vercel/remix-builder": "2.1.5",
|
||||
"@vercel/ruby": "2.1.0",
|
||||
"@vercel/static-build": "2.5.1",
|
||||
"@vercel/static-build": "2.5.3",
|
||||
"chokidar": "3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,7 +10,7 @@ export const buildCommand: Command = {
|
||||
name: 'prod',
|
||||
description: 'Build a production deployment',
|
||||
shorthand: null,
|
||||
type: String,
|
||||
type: Boolean,
|
||||
deprecated: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ import type { VercelConfig } from '@vercel/client';
|
||||
import pull from '../pull';
|
||||
import { staticFiles as getFiles } from '../../util/get-files';
|
||||
import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import { parseArguments } from '../../util/get-args';
|
||||
import cmd from '../../util/output/cmd';
|
||||
import * as cli from '../../util/pkg-name';
|
||||
import cliPkg from '../../util/pkg';
|
||||
@@ -66,12 +66,13 @@ import {
|
||||
import { importBuilders } from '../../util/build/import-builders';
|
||||
import { initCorepack, cleanupCorepack } from '../../util/build/corepack';
|
||||
import { sortBuilders } from '../../util/build/sort-builders';
|
||||
import { toEnumerableError } from '../../util/error';
|
||||
import { handleError, toEnumerableError } from '../../util/error';
|
||||
import { validateConfig } from '../../util/validate-config';
|
||||
import { setMonorepoDefaultSettings } from '../../util/build/monorepo';
|
||||
import { help } from '../help';
|
||||
import { buildCommand } from './command';
|
||||
import { scrubArgv } from '../../util/build/scrub-argv';
|
||||
import { getFlagsSpecification } from '../../util/get-flags-specification';
|
||||
|
||||
type BuildResult = BuildResultV2 | BuildResultV3;
|
||||
|
||||
@@ -133,22 +134,26 @@ export default async function main(client: Client): Promise<number> {
|
||||
process.env.__VERCEL_BUILD_RUNNING = '1';
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
const argv = getArgs(client.argv.slice(2), {
|
||||
'--output': String,
|
||||
'--prod': Boolean,
|
||||
'--yes': Boolean,
|
||||
'-y': '--yes',
|
||||
});
|
||||
let parsedArgs = null;
|
||||
|
||||
if (argv['--help']) {
|
||||
const flagsSpecification = getFlagsSpecification(buildCommand.options);
|
||||
|
||||
// Parse CLI args
|
||||
try {
|
||||
parsedArgs = parseArguments(client.argv.slice(2), flagsSpecification);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (parsedArgs.flags['--help']) {
|
||||
output.print(help(buildCommand, { columns: client.stderr.columns }));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Build `target` influences which environment variables will be used
|
||||
const target = argv['--prod'] ? 'production' : 'preview';
|
||||
const yes = Boolean(argv['--yes']);
|
||||
const target = parsedArgs.flags['--prod'] ? 'production' : 'preview';
|
||||
const yes = Boolean(parsedArgs.flags['--yes']);
|
||||
|
||||
try {
|
||||
await validateNpmrc(cwd);
|
||||
@@ -213,8 +218,8 @@ export default async function main(client: Client): Promise<number> {
|
||||
|
||||
// Delete output directory from potential previous build
|
||||
const defaultOutputDir = join(cwd, projectRootDirectory, OUTPUT_DIR);
|
||||
const outputDir = argv['--output']
|
||||
? resolve(argv['--output'])
|
||||
const outputDir = parsedArgs.flags['--output']
|
||||
? resolve(parsedArgs.flags['--output'])
|
||||
: defaultOutputDir;
|
||||
await Promise.all([
|
||||
fs.remove(outputDir),
|
||||
|
||||
7
packages/functions/CHANGELOG.md
Normal file
7
packages/functions/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @vercel/functions
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Initial release ([#11553](https://github.com/vercel/vercel/pull/11553))
|
||||
17
packages/functions/index.d.ts
vendored
Normal file
17
packages/functions/index.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Extends the lifetime of the request handler for the lifetime of the given {@link Promise}
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil
|
||||
*
|
||||
* @param promise The promise to wait for.
|
||||
* @example
|
||||
*
|
||||
* ```
|
||||
* import { waitUntil } from '@vercel/functions';
|
||||
*
|
||||
* export function GET(request) {
|
||||
* waitUntil(fetch('https://vercel.com'));
|
||||
* return new Response('OK');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const waitUntil: (promise: Promise<unknown>) => void;
|
||||
24
packages/functions/index.js
Normal file
24
packages/functions/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* global globalThis */
|
||||
|
||||
export const waitUntil = promise => {
|
||||
if (
|
||||
promise instanceof Promise === false ||
|
||||
promise === null ||
|
||||
typeof promise !== 'object' ||
|
||||
typeof promise.then !== 'function'
|
||||
) {
|
||||
throw new TypeError(
|
||||
`waitUntil can only be called with a Promise, got ${typeof promise}`
|
||||
);
|
||||
}
|
||||
|
||||
const ctx = globalThis[Symbol.for('@vercel/request-context')]?.get?.() ?? {};
|
||||
|
||||
if (!ctx.waitUntil) {
|
||||
throw new Error(
|
||||
'failed to get waitUntil function for this request context'
|
||||
);
|
||||
}
|
||||
|
||||
ctx.waitUntil(promise);
|
||||
};
|
||||
45
packages/functions/index.test.js
Normal file
45
packages/functions/index.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/* global globalThis */
|
||||
|
||||
import { vi, expect, test } from 'vitest';
|
||||
|
||||
import { waitUntil } from '.';
|
||||
|
||||
test.each([
|
||||
{},
|
||||
() => {},
|
||||
function () {},
|
||||
NaN,
|
||||
1,
|
||||
false,
|
||||
undefined,
|
||||
null,
|
||||
[],
|
||||
'▲',
|
||||
])('waitUntil throws when called with %s', input => {
|
||||
expect(() => waitUntil(input)).toThrow(TypeError);
|
||||
expect(() => waitUntil(input)).toThrow(
|
||||
`waitUntil can only be called with a Promise, got ${typeof input}`
|
||||
);
|
||||
});
|
||||
|
||||
test.each([null, undefined, {}])(
|
||||
'waitUntil throws when context is %s',
|
||||
input => {
|
||||
const promise = Promise.resolve();
|
||||
globalThis[Symbol.for('@vercel/request-context')] = input;
|
||||
expect(() => waitUntil(promise)).toThrow(Error);
|
||||
expect(() => waitUntil(promise)).toThrow(
|
||||
'failed to get waitUntil function for this request context'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test('waitUntil calls ctx.waitUntil when available', async () => {
|
||||
const promise = Promise.resolve();
|
||||
const waitUntilMock = vi.fn().mockReturnValue(promise);
|
||||
globalThis[Symbol.for('@vercel/request-context')] = {
|
||||
get: () => ({ waitUntil: waitUntilMock }),
|
||||
};
|
||||
waitUntil(promise);
|
||||
expect(waitUntilMock).toHaveBeenCalledWith(promise);
|
||||
});
|
||||
33
packages/functions/package.json
Normal file
33
packages/functions/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@vercel/functions",
|
||||
"description": "Runtime functions to be used with your Vercel Functions",
|
||||
"homepage": "https://vercel.com",
|
||||
"version": "1.0.0",
|
||||
"types": "index.d.ts",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"directory": "packages/functions",
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel/vercel.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel/vercel/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"index.js"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,37 @@
|
||||
# @vercel/next
|
||||
|
||||
## 4.2.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix missing initial RSC headers ([#11552](https://github.com/vercel/vercel/pull/11552))
|
||||
|
||||
- Remove .prefetch.rsc rewrites for non-PPR ([#11540](https://github.com/vercel/vercel/pull/11540))
|
||||
|
||||
- [next] rename middleware manifest env ([#11549](https://github.com/vercel/vercel/pull/11549))
|
||||
|
||||
## 4.2.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix missing .rsc outputs for pages prerenders ([#11503](https://github.com/vercel/vercel/pull/11503))
|
||||
|
||||
- Fix base path app index RSC handling ([#11528](https://github.com/vercel/vercel/pull/11528))
|
||||
|
||||
- Fix interception routes PPR route handling ([#11527](https://github.com/vercel/vercel/pull/11527))
|
||||
|
||||
## 4.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [next] Update test fixture version ([#11514](https://github.com/vercel/vercel/pull/11514))
|
||||
|
||||
## 4.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Only rewrite next-action requests to `.action` handlers ([#11504](https://github.com/vercel/vercel/pull/11504))
|
||||
|
||||
## 4.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "4.2.4",
|
||||
"version": "4.2.8",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
|
||||
@@ -1928,6 +1928,7 @@ export const build: BuildV2 = async ({
|
||||
bypassToken: prerenderManifest.bypassToken || '',
|
||||
isServerMode,
|
||||
experimentalPPRRoutes,
|
||||
hasActionOutputSupport: false,
|
||||
}).then(arr =>
|
||||
localizeDynamicRoutes(
|
||||
arr,
|
||||
@@ -1958,6 +1959,7 @@ export const build: BuildV2 = async ({
|
||||
bypassToken: prerenderManifest.bypassToken || '',
|
||||
isServerMode,
|
||||
experimentalPPRRoutes,
|
||||
hasActionOutputSupport: false,
|
||||
}).then(arr =>
|
||||
arr.map(route => {
|
||||
route.src = route.src.replace('^', `^${dynamicPrefix}`);
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
normalizePrefetches,
|
||||
CreateLambdaFromPseudoLayersOptions,
|
||||
getPostponeResumePathname,
|
||||
LambdaGroup,
|
||||
MAX_UNCOMPRESSED_LAMBDA_SIZE,
|
||||
} from './utils';
|
||||
import {
|
||||
@@ -68,6 +69,7 @@ const CORRECT_NOT_FOUND_ROUTES_VERSION = 'v12.0.1';
|
||||
const CORRECT_MIDDLEWARE_ORDER_VERSION = 'v12.1.7-canary.29';
|
||||
const NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION = 'v12.1.7-canary.33';
|
||||
const EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION = 'v12.2.0';
|
||||
const ACTION_OUTPUT_SUPPORT_VERSION = 'v14.2.2';
|
||||
const CORRECTED_MANIFESTS_VERSION = 'v12.2.0';
|
||||
|
||||
// Ideally this should be in a Next.js manifest so we can change it in
|
||||
@@ -199,6 +201,9 @@ export async function serverBuild({
|
||||
nextVersion,
|
||||
EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION
|
||||
);
|
||||
const hasActionOutputSupport =
|
||||
semver.gte(nextVersion, ACTION_OUTPUT_SUPPORT_VERSION) &&
|
||||
Boolean(process.env.NEXT_EXPERIMENTAL_STREAMING_ACTIONS);
|
||||
const projectDir = requiredServerFilesManifest.relativeAppDir
|
||||
? path.join(baseDir, requiredServerFilesManifest.relativeAppDir)
|
||||
: requiredServerFilesManifest.appDir || entryPath;
|
||||
@@ -246,7 +251,7 @@ export async function serverBuild({
|
||||
if (rewrite.src && rewrite.dest) {
|
||||
rewrite.src = rewrite.src.replace(
|
||||
/\/?\(\?:\/\)\?/,
|
||||
'(?<rscsuff>(\\.prefetch)?\\.rsc)?(?:/)?'
|
||||
`(?<rscsuff>${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
|
||||
);
|
||||
let destQueryIndex = rewrite.dest.indexOf('?');
|
||||
|
||||
@@ -926,11 +931,23 @@ export async function serverBuild({
|
||||
inversedAppPathManifest,
|
||||
});
|
||||
|
||||
const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];
|
||||
|
||||
for (const group of appRouterLambdaGroups) {
|
||||
if (!group.isPrerenders || group.isExperimentalPPR) {
|
||||
group.isStreaming = true;
|
||||
}
|
||||
group.isAppRouter = true;
|
||||
|
||||
// We create a streaming variant of the Prerender lambda group
|
||||
// to support actions that are part of a Prerender
|
||||
if (hasActionOutputSupport) {
|
||||
appRouterStreamingActionLambdaGroups.push({
|
||||
...group,
|
||||
isActionLambda: true,
|
||||
isStreaming: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of appRouteHandlersLambdaGroups) {
|
||||
@@ -982,6 +999,13 @@ export async function serverBuild({
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
appRouterStreamingPrerenderLambdaGroups:
|
||||
appRouterStreamingActionLambdaGroups.map(group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
appRouteHandlersLambdaGroups: appRouteHandlersLambdaGroups.map(
|
||||
group => ({
|
||||
pages: group.pages,
|
||||
@@ -999,6 +1023,7 @@ export async function serverBuild({
|
||||
const combinedGroups = [
|
||||
...pageLambdaGroups,
|
||||
...appRouterLambdaGroups,
|
||||
...appRouterStreamingActionLambdaGroups,
|
||||
...apiLambdaGroups,
|
||||
...appRouteHandlersLambdaGroups,
|
||||
];
|
||||
@@ -1208,6 +1233,11 @@ export async function serverBuild({
|
||||
|
||||
let outputName = path.posix.join(entryDirectory, pageName);
|
||||
|
||||
if (group.isActionLambda) {
|
||||
// give the streaming prerenders a .action suffix
|
||||
outputName = `${outputName}.action`;
|
||||
}
|
||||
|
||||
// If this is a PPR page, then we should prefix the output name.
|
||||
if (isPPR) {
|
||||
if (!revalidate) {
|
||||
@@ -1378,6 +1408,7 @@ export async function serverBuild({
|
||||
isServerMode: true,
|
||||
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
|
||||
experimentalPPRRoutes,
|
||||
hasActionOutputSupport,
|
||||
}).then(arr =>
|
||||
localizeDynamicRoutes(
|
||||
arr,
|
||||
@@ -1557,10 +1588,19 @@ export async function serverBuild({
|
||||
|
||||
if (lambdas[pathname]) {
|
||||
lambdas[`${pathname}.rsc`] = lambdas[pathname];
|
||||
|
||||
if (experimental.ppr) {
|
||||
lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname];
|
||||
}
|
||||
}
|
||||
|
||||
if (edgeFunctions[pathname]) {
|
||||
edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname];
|
||||
|
||||
if (experimental.ppr) {
|
||||
edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] =
|
||||
edgeFunctions[pathname];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1863,7 +1903,7 @@ export async function serverBuild({
|
||||
|
||||
...(appDir
|
||||
? [
|
||||
...(rscPrefetchHeader
|
||||
...(rscPrefetchHeader && experimental.ppr
|
||||
? [
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
@@ -1905,9 +1945,44 @@ export async function serverBuild({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(hasActionOutputSupport
|
||||
? [
|
||||
// Create rewrites for streaming prerenders (.action routes)
|
||||
// This contains separate rewrites for each possible "has" (action header, or content-type)
|
||||
// Also includes separate handling for index routes which should match to /index.action.
|
||||
// This follows the same pattern as the rewrites for .rsc files.
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
dest: path.posix.join('/', entryDirectory, '/index.action'),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'next-action',
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
src: `^${path.posix.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
'/((?!.+\\.action).+?)(?:/)?$'
|
||||
)}`,
|
||||
dest: path.posix.join('/', entryDirectory, '/$1.action'),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'next-action',
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
src: `^${path.posix.join('/', entryDirectory, '/?')}`,
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
@@ -1970,47 +2045,6 @@ export async function serverBuild({
|
||||
]
|
||||
: []),
|
||||
|
||||
...(rscPrefetchHeader && !experimental.ppr
|
||||
? [
|
||||
{
|
||||
src: path.posix.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
`/__index${RSC_PREFETCH_SUFFIX}`
|
||||
),
|
||||
dest: path.posix.join('/', entryDirectory, '/index.rsc'),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: rscPrefetchHeader,
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
src: `^${path.posix.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
`/(.+?)${RSC_PREFETCH_SUFFIX}(?:/)?$`
|
||||
)}`,
|
||||
dest: path.posix.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
`/$1${experimental.ppr ? RSC_PREFETCH_SUFFIX : '.rsc'}`
|
||||
),
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: rscPrefetchHeader,
|
||||
},
|
||||
],
|
||||
continue: true,
|
||||
override: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
// These need to come before handle: miss or else they are grouped
|
||||
// with that routing section
|
||||
...afterFilesRewrites,
|
||||
|
||||
@@ -321,6 +321,7 @@ export async function getDynamicRoutes({
|
||||
isServerMode,
|
||||
dynamicMiddlewareRouteMap,
|
||||
experimentalPPRRoutes,
|
||||
hasActionOutputSupport,
|
||||
}: {
|
||||
entryPath: string;
|
||||
entryDirectory: string;
|
||||
@@ -333,6 +334,7 @@ export async function getDynamicRoutes({
|
||||
isServerMode?: boolean;
|
||||
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
|
||||
experimentalPPRRoutes: ReadonlySet<string>;
|
||||
hasActionOutputSupport: boolean;
|
||||
}): Promise<RouteWithSrc[]> {
|
||||
if (routesManifest) {
|
||||
switch (routesManifest.version) {
|
||||
@@ -423,14 +425,25 @@ export async function getDynamicRoutes({
|
||||
});
|
||||
}
|
||||
|
||||
routes.push({
|
||||
...route,
|
||||
src: route.src.replace(
|
||||
new RegExp(escapeStringRegexp('(?:/)?$')),
|
||||
'(?:\\.rsc)(?:/)?$'
|
||||
),
|
||||
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
||||
});
|
||||
if (hasActionOutputSupport) {
|
||||
routes.push({
|
||||
...route,
|
||||
src: route.src.replace(
|
||||
new RegExp(escapeStringRegexp('(?:/)?$')),
|
||||
'(?<nxtsuffix>(?:\\.action|\\.rsc))(?:/)?$'
|
||||
),
|
||||
dest: route.dest?.replace(/($|\?)/, '$nxtsuffix$1'),
|
||||
});
|
||||
} else {
|
||||
routes.push({
|
||||
...route,
|
||||
src: route.src.replace(
|
||||
new RegExp(escapeStringRegexp('(?:/)?$')),
|
||||
'(?:\\.rsc)(?:/)?$'
|
||||
),
|
||||
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
||||
});
|
||||
}
|
||||
|
||||
routes.push(route);
|
||||
}
|
||||
@@ -1487,6 +1500,7 @@ export type LambdaGroup = {
|
||||
isStreaming?: boolean;
|
||||
isPrerenders?: boolean;
|
||||
isExperimentalPPR?: boolean;
|
||||
isActionLambda?: boolean;
|
||||
isPages?: boolean;
|
||||
isApiLambda: boolean;
|
||||
pseudoLayer: PseudoLayer;
|
||||
@@ -2451,6 +2465,7 @@ export const onPrerenderRoute =
|
||||
...(rscEnabled
|
||||
? {
|
||||
initialHeaders: {
|
||||
...initialHeaders,
|
||||
'content-type': rscContentTypeHeader,
|
||||
vary: rscVaryHeader,
|
||||
// If it contains a pre-render, then it was postponed.
|
||||
@@ -2463,6 +2478,22 @@ export const onPrerenderRoute =
|
||||
});
|
||||
}
|
||||
|
||||
// we need to ensure all prerenders have a matching .rsc output
|
||||
// otherwise routing could fall through unexpectedly for the
|
||||
// fallback: false case as it doesn't have a dynamic route
|
||||
// to catch the `.rsc` request for app -> pages routing
|
||||
if (outputPrerenderPathData?.endsWith('.json') && appDir) {
|
||||
const dummyOutput = new FileBlob({
|
||||
data: '{}',
|
||||
contentType: 'application/json',
|
||||
});
|
||||
const rscKey = `${outputPathPage}.rsc`;
|
||||
const prefetchRscKey = `${outputPathPage}${RSC_PREFETCH_SUFFIX}`;
|
||||
|
||||
prerenders[rscKey] = dummyOutput;
|
||||
prerenders[prefetchRscKey] = dummyOutput;
|
||||
}
|
||||
|
||||
++prerenderGroup;
|
||||
|
||||
if (routesManifest?.i18n && isBlocking) {
|
||||
@@ -2754,7 +2785,7 @@ interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
|
||||
|
||||
interface EdgeFunctionInfoV3 extends BaseEdgeFunctionInfo {
|
||||
matchers: EdgeFunctionMatcher[];
|
||||
environments: Record<string, string>;
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
interface EdgeFunctionMatcher {
|
||||
@@ -2989,7 +3020,7 @@ export async function getMiddlewareBundle({
|
||||
slug: 'nextjs',
|
||||
version: nextVersion,
|
||||
},
|
||||
environment: edgeFunction.environments,
|
||||
environment: edgeFunction.env,
|
||||
});
|
||||
})(),
|
||||
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
|
||||
@@ -3169,7 +3200,7 @@ export function upgradeMiddlewareManifestV1(
|
||||
return {
|
||||
...rest,
|
||||
matchers: [{ regexp }],
|
||||
environments: {},
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3197,7 +3228,7 @@ export function upgradeMiddlewareManifestV2(
|
||||
const { ...rest } = v2Info;
|
||||
return {
|
||||
...rest,
|
||||
environments: {},
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
6
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/actions.js
vendored
Normal file
6
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/actions.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use server';
|
||||
|
||||
export async function increment(value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return value + 1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { increment } from '../../../actions';
|
||||
|
||||
export default function Page() {
|
||||
const [count, setCount] = useState(0);
|
||||
async function updateCount() {
|
||||
const newCount = await increment(count);
|
||||
setCount(newCount);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={updateCount}>
|
||||
<div id="count">{count}</div>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { increment } from '../../../actions';
|
||||
|
||||
export default function Page() {
|
||||
const [count, setCount] = useState(0);
|
||||
async function updateCount() {
|
||||
const newCount = await increment(count);
|
||||
setCount(newCount);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={updateCount}>
|
||||
<div id="count">{count}</div>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
19
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/client/static/page.js
vendored
Normal file
19
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/client/static/page.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { increment } from '../../actions';
|
||||
|
||||
export default function Page() {
|
||||
const [count, setCount] = useState(0);
|
||||
async function updateCount() {
|
||||
const newCount = await increment(count);
|
||||
setCount(newCount);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={updateCount}>
|
||||
<div id="count">{count}</div>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/layout.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/layout.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
45
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/other/page.js
vendored
Normal file
45
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/other/page.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
// @ts-ignore
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
function request(method) {
|
||||
return fetch('/api/test', {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data;.*',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [result, setResult] = useState('Press submit');
|
||||
const onClick = useCallback(async method => {
|
||||
const res = await request(method);
|
||||
const text = await res.text();
|
||||
|
||||
setResult(res.ok ? `${method} ${text}` : 'Error: ' + res.status);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<div className="flex flex-row space-x-2 items-center justify-center">
|
||||
<button
|
||||
className="border border-white rounded-sm p-4 mb-4"
|
||||
onClick={() => onClick('GET')}
|
||||
>
|
||||
Submit GET
|
||||
</button>
|
||||
<button
|
||||
className="border border-white rounded-sm p-4 mb-4"
|
||||
onClick={() => onClick('POST')}
|
||||
>
|
||||
Submit POST
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-white">{result}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/dynamic/page.js
vendored
Normal file
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/dynamic/page.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function Page() {
|
||||
async function serverAction() {
|
||||
'use server';
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
revalidatePath('/dynamic');
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={serverAction}>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function Page() {
|
||||
async function serverAction() {
|
||||
'use server';
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
revalidatePath('/dynamic');
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={serverAction}>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function Page() {
|
||||
async function serverAction() {
|
||||
'use server';
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
revalidatePath('/dynamic');
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={serverAction}>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ slug: 'pre-generated' }];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return children;
|
||||
}
|
||||
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/static/page.js
vendored
Normal file
15
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/app/rsc/static/page.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export default async function Page() {
|
||||
async function serverAction() {
|
||||
'use server';
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
revalidatePath('/dynamic');
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={serverAction}>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
236
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
vendored
Normal file
236
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/index.test.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
/* eslint-env jest */
|
||||
const path = require('path');
|
||||
const { deployAndTest } = require('../../utils');
|
||||
const fetch = require('../../../../../test/lib/deployment/fetch-retry');
|
||||
|
||||
const ctx = {};
|
||||
|
||||
function findActionId(page) {
|
||||
page = `app${page}/page`; // add /app prefix and /page suffix
|
||||
|
||||
for (const [actionId, details] of Object.entries(ctx.actionManifest.node)) {
|
||||
if (details.workers[page]) {
|
||||
return actionId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateFormDataPayload(actionId) {
|
||||
return {
|
||||
method: 'POST',
|
||||
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
|
||||
'Next-Action': actionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
beforeAll(async () => {
|
||||
const info = await deployAndTest(__dirname);
|
||||
|
||||
const actionManifest = await fetch(
|
||||
`${info.deploymentUrl}/server-reference-manifest.json`
|
||||
).then(res => res.json());
|
||||
|
||||
ctx.actionManifest = actionManifest;
|
||||
|
||||
Object.assign(ctx, info);
|
||||
});
|
||||
|
||||
describe('client component', () => {
|
||||
it('should bypass the static cache for a server action', async () => {
|
||||
const path = '/client/static';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify([1337]),
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'Next-Action': actionId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
const body = await res.text();
|
||||
expect(body).toContain('1338');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
|
||||
const path = '/client/static/[dynamic-static]';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify([1337]),
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'Next-Action': actionId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
const body = await res.text();
|
||||
expect(body).toContain('1338');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a multipart request (no action header)', async () => {
|
||||
const path = '/client/static';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
|
||||
method: 'POST',
|
||||
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||
// This is a "BYPASS" because no action ID was provided, so it'll fall back to
|
||||
// `experimentalBypassFor` handling.
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
});
|
||||
|
||||
it('should properly invoke the action on a dynamic page', async () => {
|
||||
const path = '/client/dynamic/[id]';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify([1337]),
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'Next-Action': actionId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
const body = await res.text();
|
||||
expect(body).toContain('1338');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('server component', () => {
|
||||
it('should bypass the static cache for a server action', async () => {
|
||||
const path = '/rsc/static';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}${path}`,
|
||||
generateFormDataPayload(actionId)
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action on a page with dynamic params', async () => {
|
||||
const path = '/rsc/static/[dynamic-static]';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}${path}`,
|
||||
generateFormDataPayload(actionId)
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
it('should properly invoke the action on a dynamic page', async () => {
|
||||
const path = '/rsc/dynamic';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}${path}`,
|
||||
generateFormDataPayload(actionId)
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path + '.action');
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
describe('generateStaticParams', () => {
|
||||
it('should bypass the static cache for a server action when pre-generated', async () => {
|
||||
const path = '/rsc/static/generate-static-params/pre-generated';
|
||||
const actionId = findActionId(
|
||||
'/rsc/static/generate-static-params/[slug]'
|
||||
);
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}${path}`,
|
||||
generateFormDataPayload(actionId)
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(
|
||||
'/rsc/static/generate-static-params/[slug].action'
|
||||
);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a server action when not pre-generated', async () => {
|
||||
const page = '/rsc/static/generate-static-params/[slug]';
|
||||
const actionId = findActionId(page);
|
||||
|
||||
const res = await fetch(
|
||||
`${ctx.deploymentUrl}/rsc/static/generate-static-params/not-pre-generated`,
|
||||
generateFormDataPayload(actionId)
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(page + '.action');
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pages', () => {
|
||||
it('should not attempt to rewrite the action path for a server action (POST)', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/api/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe('/api/test');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ message: 'Hello from Next.js!' });
|
||||
});
|
||||
|
||||
it('should not attempt to rewrite the action path for a server action (GET)', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/api/test`);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe('/api/test');
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ message: 'Hello from Next.js!' });
|
||||
});
|
||||
});
|
||||
});
|
||||
1
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
vendored
Normal file
1
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/next.config.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
9
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/package.json
vendored
Normal file
9
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/package.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "next build && cp .next/server/server-reference-manifest.json public/"
|
||||
},
|
||||
"ignoreNextjsUpdates": true
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/pages/api/test.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/pages/api/test.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ message: 'Hello from Next.js!' });
|
||||
}
|
||||
0
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/public/.keep
vendored
Normal file
0
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/public/.keep
vendored
Normal file
14
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/vercel.json
vendored
Normal file
14
packages/next/test/fixtures/00-app-dir-actions-experimental-streaming/vercel.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@vercel/next"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"env": {
|
||||
"NEXT_EXPERIMENTAL_STREAMING_ACTIONS": "1"
|
||||
}
|
||||
},
|
||||
"probes": []
|
||||
}
|
||||
@@ -7,4 +7,4 @@ export default function Root({ children }) {
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
45
packages/next/test/fixtures/00-app-dir-actions/app/other/page.js
vendored
Normal file
45
packages/next/test/fixtures/00-app-dir-actions/app/other/page.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
// @ts-ignore
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
function request(method) {
|
||||
return fetch('/api/test', {
|
||||
method,
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data;.*',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [result, setResult] = useState('Press submit');
|
||||
const onClick = useCallback(async method => {
|
||||
const res = await request(method);
|
||||
const text = await res.text();
|
||||
|
||||
setResult(res.ok ? `${method} ${text}` : 'Error: ' + res.status);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<div className="flex flex-row space-x-2 items-center justify-center">
|
||||
<button
|
||||
className="border border-white rounded-sm p-4 mb-4"
|
||||
onClick={() => onClick('GET')}
|
||||
>
|
||||
Submit GET
|
||||
</button>
|
||||
<button
|
||||
className="border border-white rounded-sm p-4 mb-4"
|
||||
onClick={() => onClick('POST')}
|
||||
>
|
||||
Submit POST
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-white">{result}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
).then(res => res.json());
|
||||
|
||||
ctx.actionManifest = actionManifest;
|
||||
|
||||
Object.assign(ctx, info);
|
||||
});
|
||||
|
||||
@@ -81,6 +82,25 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
|
||||
it('should bypass the static cache for a multipart request (no action header)', async () => {
|
||||
const path = '/client/static';
|
||||
const actionId = findActionId(path);
|
||||
|
||||
const res = await fetch(`${ctx.deploymentUrl}${path}`, {
|
||||
method: 'POST',
|
||||
body: `------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"1_$ACTION_ID_${actionId}\"\r\n\r\n\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n[\"$K1\"]\r\n------WebKitFormBoundaryHcVuFa30AN0QV3uZ--\r\n`,
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
});
|
||||
|
||||
it('should properly invoke the action on a dynamic page', async () => {
|
||||
const path = '/client/dynamic/[id]';
|
||||
const actionId = findActionId(path);
|
||||
@@ -98,6 +118,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
const body = await res.text();
|
||||
expect(body).toContain('1338');
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
// This isn't a "BYPASS" because the action wasn't part of a static prerender
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
});
|
||||
@@ -145,6 +166,7 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
// This isn't a "BYPASS" because the action wasn't part of a static prerender
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
});
|
||||
|
||||
@@ -161,7 +183,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(path);
|
||||
expect(res.headers.get('x-matched-path')).toBe(
|
||||
'/rsc/static/generate-static-params/pre-generated'
|
||||
);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
@@ -178,8 +202,36 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe(page);
|
||||
expect(res.headers.get('content-type')).toBe('text/x-component');
|
||||
// This isn't a "BYPASS" because the action wasn't part of a static prerender
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('BYPASS');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pages', () => {
|
||||
it('should not attempt to rewrite the action path for a server action (POST)', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/api/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'multipart/form-data; boundary=----WebKitFormBoundaryHcVuFa30AN0QV3uZ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe('/api/test');
|
||||
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ message: 'Hello from Next.js!' });
|
||||
});
|
||||
|
||||
it('should not attempt to rewrite the action path for a server action (GET)', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/api/test`);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.headers.get('x-matched-path')).toBe('/api/test');
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ message: 'Hello from Next.js!' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1
packages/next/test/fixtures/00-app-dir-actions/next.config.js
vendored
Normal file
1
packages/next/test/fixtures/00-app-dir-actions/next.config.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
3
packages/next/test/fixtures/00-app-dir-actions/pages/api/test.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-actions/pages/api/test.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ message: 'Hello from Next.js!' });
|
||||
}
|
||||
@@ -18,6 +18,31 @@
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/hello/world",
|
||||
"status": 200,
|
||||
"mustContain": "index app page"
|
||||
},
|
||||
{
|
||||
"path": "/hello/world",
|
||||
"status": 200,
|
||||
"mustContain": "index app page",
|
||||
"mustNotContain": "<html",
|
||||
"headers": {
|
||||
"RSC": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/hello/world",
|
||||
"status": 200,
|
||||
"mustContain": ":",
|
||||
"mustNotContain": "<html",
|
||||
"headers": {
|
||||
"RSC": 1,
|
||||
"Next-Router-Prefetch": 1
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "/hello/world/dashboard/hello",
|
||||
"status": 200,
|
||||
@@ -42,14 +67,14 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from /ssg",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/hello/world/ssg",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -82,14 +107,14 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from app/dashboard/deployments/[id]/settings",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/hello/world/dashboard/deployments/123/settings",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -102,14 +127,14 @@
|
||||
"status": 200,
|
||||
"mustContain": "catchall",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/hello/world/dashboard/deployments/catchall/something",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -122,7 +147,7 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from app/dashboard",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -142,7 +167,7 @@
|
||||
},
|
||||
"responseHeaders": {
|
||||
"content-type": "text/x-component",
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
5
packages/next/test/fixtures/00-app-dir-no-ppr/app/nested/[...rest]/page.js
vendored
Normal file
5
packages/next/test/fixtures/00-app-dir-no-ppr/app/nested/[...rest]/page.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<p>nested app router catch-all</p>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function Page() {
|
||||
return <p>index app page {Date.now()}</p>;
|
||||
}
|
||||
|
||||
7
packages/next/test/fixtures/00-app-dir-no-ppr/app/products/[productId]/page.jsx
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-no-ppr/app/products/[productId]/page.jsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
product page
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,10 @@ module.exports = {
|
||||
source: '/rewritten-to-index',
|
||||
destination: '/?fromRewrite=1',
|
||||
},
|
||||
{
|
||||
source: '/to-product/:productId.html',
|
||||
destination: '/products/:productId',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "13.5.6",
|
||||
"next": "canary",
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental"
|
||||
},
|
||||
|
||||
24
packages/next/test/fixtures/00-app-dir-no-ppr/pages/nested/blog-fallback-false/[slug].js
vendored
Normal file
24
packages/next/test/fixtures/00-app-dir-no-ppr/pages/nested/blog-fallback-false/[slug].js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Page(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from /nested/blog-fallback-false/[slug]</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
now: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
{ params: { slug: 'first' } },
|
||||
],
|
||||
fallback: false
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,74 @@
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/to-product/product-id.html",
|
||||
"status": 200,
|
||||
"mustContain": "<html"
|
||||
},
|
||||
{
|
||||
"path": "/to-product/product-id.html",
|
||||
"status": 200,
|
||||
"mustNotContain": "<html",
|
||||
"mustContain": ":",
|
||||
"headers": {
|
||||
"RSC": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/to-product/product-id.html",
|
||||
"status": 200,
|
||||
"mustNotContain": ".prefetch",
|
||||
"mustContain": ":",
|
||||
"headers": {
|
||||
"RSC": 1,
|
||||
"Next-Router-Prefetch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nested/blog-fallback-false/first",
|
||||
"status": 200,
|
||||
"mustContain": "hello from /nested/blog-fallback-false"
|
||||
},
|
||||
{
|
||||
"path": "/nested/blog-fallback-false/first",
|
||||
"status": 200,
|
||||
"mustContain": "{}",
|
||||
"mustNotContain": ":",
|
||||
"responseHeaders": {
|
||||
"x-matched-path": "/nested/blog-fallback-false/first.rsc"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": 1,
|
||||
"Next-Router-Prefetch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nested/blog-fallback-false/first",
|
||||
"status": 200,
|
||||
"mustContain": "{}",
|
||||
"mustNotContain": ":",
|
||||
"responseHeaders": {
|
||||
"x-matched-path": "/nested/blog-fallback-false/first.rsc"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/nested/blog-fallback-false/non-existent",
|
||||
"status": 200,
|
||||
"mustContain": "nested app router catch-all"
|
||||
},
|
||||
{
|
||||
"path": "/nested/blog-fallback-false/non-existent",
|
||||
"status": 200,
|
||||
"mustContain": ":",
|
||||
"headers": {
|
||||
"RSC": 1,
|
||||
"Next-Router-Prefetch": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/dynamic-index/hello/index",
|
||||
"status": 200,
|
||||
@@ -156,7 +224,7 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from /ssg",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -185,7 +253,7 @@
|
||||
"path": "/ssg",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -197,7 +265,7 @@
|
||||
"path": "/ssg",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url",
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch",
|
||||
"content-type": "text/x-component"
|
||||
},
|
||||
"headers": {
|
||||
@@ -232,14 +300,14 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from app/dashboard/deployments/[id]/settings",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/dashboard/deployments/123/settings",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -252,14 +320,14 @@
|
||||
"status": 200,
|
||||
"mustContain": "catchall",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/dashboard/deployments/catchall/something",
|
||||
"status": 200,
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
},
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
@@ -272,7 +340,7 @@
|
||||
"status": 200,
|
||||
"mustContain": "hello from app/dashboard",
|
||||
"responseHeaders": {
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -292,7 +360,7 @@
|
||||
},
|
||||
"responseHeaders": {
|
||||
"content-type": "text/x-component",
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url"
|
||||
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/(.)cart/page.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/(.)cart/page.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Modal() {
|
||||
return (
|
||||
<>
|
||||
<p>cart modal!!</p>
|
||||
<Link href='/'>to index</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/default.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/@modal/default.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Modal() {
|
||||
return null
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/cart/page.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/cart/page.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Cart() {
|
||||
return (
|
||||
<>
|
||||
<p>normal cart page</p>
|
||||
<Link href='/'>to index</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/layout.js
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-ppr-full/app/(store)/layout.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function Layout({ modal, children }) {
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import Link from 'next/link'
|
||||
import React, { Suspense } from 'react'
|
||||
import { Dynamic } from '../components/dynamic'
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<Suspense fallback={<Dynamic pathname="/" fallback />}>
|
||||
<Dynamic pathname="/" />
|
||||
</Suspense>
|
||||
<>
|
||||
<Suspense fallback={<Dynamic pathname="/" fallback />}>
|
||||
<Dynamic pathname="/" />
|
||||
</Suspense>
|
||||
<Link href='/cart'>to /cart</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,6 @@ const links = [
|
||||
{ href: '/no-suspense/nested/c', tag: 'no suspense, on-demand' },
|
||||
{ href: '/dynamic/force-dynamic', tag: "dynamic = 'force-dynamic'" },
|
||||
{ href: '/dynamic/force-static', tag: "dynamic = 'force-static'" },
|
||||
{ href: '/edge/suspense', tag: 'edge, pre-generated' },
|
||||
{ href: '/edge/suspense/a', tag: 'edge, pre-generated' },
|
||||
{ href: '/edge/suspense/b', tag: 'edge, on-demand' },
|
||||
{ href: '/edge/suspense/c', tag: 'edge, on-demand' },
|
||||
{ href: '/edge/no-suspense', tag: 'edge, no suspense, pre-generated' },
|
||||
{ href: '/edge/no-suspense/a', tag: 'edge, no suspense, pre-generated' },
|
||||
{ href: '/edge/no-suspense/b', tag: 'edge, no suspense, on-demand' },
|
||||
{ href: '/edge/no-suspense/c', tag: 'edge, no suspense, on-demand' },
|
||||
];
|
||||
|
||||
export const Links = () => {
|
||||
|
||||
@@ -44,6 +44,45 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
Object.assign(ctx, info);
|
||||
});
|
||||
|
||||
it('should handle interception route properly', async () => {
|
||||
const res = await fetch(`${ctx.deploymentUrl}/cart`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toContain('normal cart page');
|
||||
|
||||
const res2 = await fetch(`${ctx.deploymentUrl}/cart`, {
|
||||
headers: {
|
||||
RSC: '1',
|
||||
},
|
||||
});
|
||||
const res2Body = await res2.text();
|
||||
expect(res2.status).toBe(200);
|
||||
expect(res2Body).toContain(':');
|
||||
expect(res2Body).not.toContain('<html');
|
||||
|
||||
const res3 = await fetch(`${ctx.deploymentUrl}/cart`, {
|
||||
headers: {
|
||||
RSC: '1',
|
||||
'Next-Url': '/cart',
|
||||
'Next-Router-Prefetch': '1',
|
||||
},
|
||||
});
|
||||
const res3Body = await res3.text();
|
||||
expect(res3.status).toBe(200);
|
||||
expect(res3Body).toContain(':');
|
||||
expect(res3Body).not.toContain('<html');
|
||||
|
||||
const res4 = await fetch(`${ctx.deploymentUrl}/cart`, {
|
||||
headers: {
|
||||
RSC: '1',
|
||||
'Next-Url': '/cart',
|
||||
},
|
||||
});
|
||||
const res4Body = await res4.text();
|
||||
expect(res4.status).toBe(200);
|
||||
expect(res4Body).toContain(':');
|
||||
expect(res4Body).not.toContain('<html');
|
||||
});
|
||||
|
||||
describe('dynamic pages should resume', () => {
|
||||
it.each(pages.filter(p => p.dynamic === true))(
|
||||
'should resume $pathname',
|
||||
|
||||
@@ -76,9 +76,9 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
|
||||
|
||||
// RSC, root-level page.js
|
||||
expect(buildResult.output['index']).toBeDefined();
|
||||
expect(buildResult.output['index'].type).toBe('Lambda');
|
||||
expect(buildResult.output['index'].memory).toBe(512);
|
||||
expect(buildResult.output['index'].maxDuration).toBe(5);
|
||||
expect(buildResult.output['index'].type).toBe('Prerender');
|
||||
expect(buildResult.output['index'].lambda.memory).toBe(512);
|
||||
expect(buildResult.output['index'].lambda.maxDuration).toBe(5);
|
||||
|
||||
expect(buildResult.output['dashboard']).toBeDefined();
|
||||
expect(buildResult.output['dashboard/another']).toBeDefined();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @vercel/node
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Make waitUntil consistent for Node.js & Edge ([#11553](https://github.com/vercel/vercel/pull/11553))
|
||||
|
||||
## 3.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/node",
|
||||
"version": "3.0.28",
|
||||
"version": "3.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -23,7 +23,7 @@
|
||||
"@edge-runtime/node-utils": "2.3.0",
|
||||
"@edge-runtime/primitives": "4.1.0",
|
||||
"@edge-runtime/vm": "3.2.0",
|
||||
"@types/node": "14.18.33",
|
||||
"@types/node": "16.18.11",
|
||||
"@vercel/build-utils": "8.0.0",
|
||||
"@vercel/error-utils": "2.0.2",
|
||||
"@vercel/nft": "0.26.4",
|
||||
@@ -50,6 +50,7 @@
|
||||
"@types/cookie": "0.3.3",
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@vercel/functions": "workspace:*",
|
||||
"content-type": "1.0.5",
|
||||
"cookie": "0.4.0",
|
||||
"cross-env": "7.0.3",
|
||||
|
||||
30
packages/node/src/awaiter.ts
Normal file
30
packages/node/src/awaiter.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* The Awaiter class is used to manage and await multiple promises.
|
||||
*/
|
||||
export class Awaiter {
|
||||
private promises: Set<Promise<unknown>> = new Set();
|
||||
private onError: ((error: Error) => void) | undefined;
|
||||
|
||||
constructor({ onError }: { onError?: (error: Error) => void } = {}) {
|
||||
this.onError = onError ?? console.error;
|
||||
}
|
||||
|
||||
public awaiting = () =>
|
||||
this.waitForBatch().then(() =>
|
||||
this.promises.size > 0 ? this.waitForBatch() : Promise.resolve()
|
||||
);
|
||||
|
||||
public waitUntil = (promise: Promise<unknown>) => {
|
||||
this.promises.add(promise);
|
||||
};
|
||||
|
||||
private waitForBatch = async () => {
|
||||
const promises = Array.from(this.promises);
|
||||
this.promises.clear();
|
||||
await Promise.all(
|
||||
promises.map(promise =>
|
||||
Promise.resolve(promise).then(() => undefined, this.onError)
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -167,7 +167,6 @@ process.on('message', async m => {
|
||||
if (onExit) {
|
||||
await onExit();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
default:
|
||||
console.error(`unknown IPC message from parent:`, m);
|
||||
|
||||
@@ -88,7 +88,12 @@ function registerFetchListener(module, options, dependencies) {
|
||||
`No default or HTTP-named export was found at ${url}. Add one to handle requests. Learn more: https://vercel.link/creating-edge-middleware`
|
||||
);
|
||||
}
|
||||
const response = await respond(handler, event, options, dependencies);
|
||||
const response = await respond(
|
||||
(req, ctx) => handler(req, { waitUntil: ctx.waitUntil.bind(ctx) }),
|
||||
event,
|
||||
options,
|
||||
dependencies
|
||||
);
|
||||
event.respondWith(response);
|
||||
} catch (error) {
|
||||
event.respondWith(toResponseError(error, dependencies.Response));
|
||||
|
||||
@@ -7,13 +7,20 @@ import { EdgeRuntime, runServer } from 'edge-runtime';
|
||||
import { type Dispatcher, Headers, request as undiciRequest } from 'undici';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
import { readFileSync } from 'fs';
|
||||
import { serializeBody, entrypointToOutputPath, logError } from '../utils.js';
|
||||
import {
|
||||
serializeBody,
|
||||
entrypointToOutputPath,
|
||||
logError,
|
||||
waitUntilWarning,
|
||||
WAIT_UNTIL_TIMEOUT_MS,
|
||||
} from '../utils.js';
|
||||
import esbuild from 'esbuild';
|
||||
import { buildToHeaders } from '@edge-runtime/node-utils';
|
||||
import type { VercelProxyResponse } from '../types.js';
|
||||
import type { IncomingMessage } from 'http';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { EdgeRuntimeServer } from 'edge-runtime/dist/server/run-server.js';
|
||||
import { Awaiter } from '../awaiter.js';
|
||||
|
||||
const NODE_VERSION_MAJOR = process.version.match(/^v(\d+)\.\d+/)?.[1];
|
||||
const NODE_VERSION_IDENTIFIER = `node${NODE_VERSION_MAJOR}`;
|
||||
@@ -41,6 +48,8 @@ async function compileUserCode(
|
||||
userCode: string;
|
||||
wasmAssets: WasmAssets;
|
||||
nodeCompatBindings: NodeCompatBindings;
|
||||
entrypointPath: string;
|
||||
awaiter: Awaiter;
|
||||
}
|
||||
> {
|
||||
const { wasmAssets, plugin: edgeWasmPlugin } = createEdgeWasmPlugin();
|
||||
@@ -110,9 +119,11 @@ async function compileUserCode(
|
||||
`;
|
||||
|
||||
return {
|
||||
entrypointPath: entrypointFullPath,
|
||||
userCode,
|
||||
wasmAssets,
|
||||
nodeCompatBindings: nodeCompatPlugin.bindings,
|
||||
awaiter: new Awaiter(),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// We can't easily show a meaningful stack trace from esbuild -> edge-runtime.
|
||||
@@ -127,6 +138,8 @@ async function createEdgeRuntimeServer(params?: {
|
||||
userCode: string;
|
||||
wasmAssets: WasmAssets;
|
||||
nodeCompatBindings: NodeCompatBindings;
|
||||
entrypointPath: string;
|
||||
awaiter: Awaiter;
|
||||
}): Promise<
|
||||
{ server: EdgeRuntimeServer; onExit: () => Promise<void> } | undefined
|
||||
> {
|
||||
@@ -157,6 +170,12 @@ async function createEdgeRuntimeServer(params?: {
|
||||
|
||||
// These are the global bindings for WebAssembly module
|
||||
...wasmBindings,
|
||||
|
||||
FetchEvent: class extends context.FetchEvent {
|
||||
waitUntil = (promise: Promise<unknown>) => {
|
||||
params!.awaiter.waitUntil(promise);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return context;
|
||||
@@ -165,31 +184,25 @@ async function createEdgeRuntimeServer(params?: {
|
||||
|
||||
const server = await runServer({ runtime });
|
||||
|
||||
const onExit = async () => {
|
||||
// When exiting this process, wait for the Edge Runtime server to finish
|
||||
// all its work, especially waitUntil promises before exiting this process.
|
||||
//
|
||||
// Here we use a short timeout (10 seconds) to let the user know that
|
||||
// it has a long-running waitUntil promise.
|
||||
const WAIT_UNTIL_TIMEOUT = 10 * 1000;
|
||||
const waitUntil = server.close();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn(
|
||||
`Edge Runtime server is still running after ${WAIT_UNTIL_TIMEOUT} ms` +
|
||||
` (hint: do you have a long-running waitUntil() promise?)`
|
||||
);
|
||||
resolve();
|
||||
}, WAIT_UNTIL_TIMEOUT);
|
||||
// @ts-expect-error
|
||||
runtime.context.globalThis[Symbol.for('@vercel/request-context')] = {
|
||||
get: () => ({
|
||||
waitUntil: params.awaiter.waitUntil.bind(params.awaiter),
|
||||
}),
|
||||
};
|
||||
|
||||
waitUntil
|
||||
const onExit = () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn(waitUntilWarning(params.entrypointPath));
|
||||
resolve();
|
||||
}, WAIT_UNTIL_TIMEOUT_MS);
|
||||
|
||||
Promise.all([params.awaiter.awaiting(), server.close()])
|
||||
.then(() => resolve())
|
||||
.catch(reject)
|
||||
.finally(() => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
.finally(() => clearTimeout(timeout));
|
||||
});
|
||||
};
|
||||
|
||||
return { server, onExit };
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export function forkDevServer(options: {
|
||||
require_: NodeRequire;
|
||||
entrypoint: string;
|
||||
meta: Meta;
|
||||
printLogs?: boolean;
|
||||
|
||||
/**
|
||||
* A path to the dev-server path. This is used in tests.
|
||||
@@ -59,9 +60,19 @@ export function forkDevServer(options: {
|
||||
: undefined,
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
}),
|
||||
stdio: options.printLogs ? 'pipe' : undefined,
|
||||
};
|
||||
const child = fork(devServerPath, [], forkOptions);
|
||||
|
||||
if (options.printLogs) {
|
||||
child.stdout?.on('data', data => {
|
||||
console.log(`stdout: ${data}`);
|
||||
});
|
||||
child.stderr?.on('data', data => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
}
|
||||
|
||||
checkForPid(devServerPath, child);
|
||||
|
||||
return child;
|
||||
|
||||
@@ -1,44 +1,51 @@
|
||||
import type { ServerResponse, IncomingMessage } from 'http';
|
||||
import type { NodeHandler } from '@edge-runtime/node-utils';
|
||||
import type { Awaiter } from '../awaiter';
|
||||
import { buildToNodeHandler } from '@edge-runtime/node-utils';
|
||||
import Edge from '@edge-runtime/primitives';
|
||||
|
||||
const webHandlerToNodeHandler = buildToNodeHandler(
|
||||
{
|
||||
Headers,
|
||||
ReadableStream,
|
||||
// @ts-expect-error Property 'duplex' is missing in type 'Request'
|
||||
Request: class extends Request {
|
||||
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
|
||||
super(input, addDuplexToInit(init));
|
||||
}
|
||||
export const createWebExportsHandler = (awaiter: Awaiter) => {
|
||||
const webHandlerToNodeHandler = buildToNodeHandler(
|
||||
{
|
||||
Headers,
|
||||
ReadableStream,
|
||||
// @ts-expect-error Property 'duplex' is missing in type 'Request'
|
||||
Request: class extends Request {
|
||||
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
|
||||
super(input, addDuplexToInit(init));
|
||||
}
|
||||
},
|
||||
Uint8Array,
|
||||
// @ts-expect-error Property 'waitUntil' is missing in type 'FetchEvent'
|
||||
FetchEvent: class {
|
||||
waitUntil = (promise: Promise<unknown>) => awaiter.waitUntil(promise);
|
||||
},
|
||||
},
|
||||
Uint8Array,
|
||||
FetchEvent: Edge.FetchEvent,
|
||||
},
|
||||
{ defaultOrigin: 'https://vercel.com' }
|
||||
);
|
||||
{ defaultOrigin: 'https://vercel.com' }
|
||||
);
|
||||
|
||||
/**
|
||||
* When users export at least one HTTP handler, we will generate
|
||||
* a generic handler routing to the right method. If there is no
|
||||
* handler function exported returns null.
|
||||
*/
|
||||
export function getWebExportsHandler(listener: any, methods: string[]) {
|
||||
const handlerByMethod: { [key: string]: NodeHandler } = {};
|
||||
/**
|
||||
* When users export at least one HTTP handler, we will generate
|
||||
* a generic handler routing to the right method. If there is no
|
||||
* handler function exported returns null.
|
||||
*/
|
||||
function getWebExportsHandler(listener: any, methods: string[]) {
|
||||
const handlerByMethod: { [key: string]: NodeHandler } = {};
|
||||
|
||||
for (const key of methods) {
|
||||
handlerByMethod[key] =
|
||||
typeof listener[key] !== 'undefined'
|
||||
? webHandlerToNodeHandler(listener[key])
|
||||
: defaultHttpHandler;
|
||||
for (const key of methods) {
|
||||
handlerByMethod[key] =
|
||||
typeof listener[key] !== 'undefined'
|
||||
? webHandlerToNodeHandler(listener[key])
|
||||
: defaultHttpHandler;
|
||||
}
|
||||
|
||||
return (req: IncomingMessage, res: ServerResponse) => {
|
||||
const method = req.method ?? 'GET';
|
||||
handlerByMethod[method](req, res);
|
||||
};
|
||||
}
|
||||
|
||||
return (req: IncomingMessage, res: ServerResponse) => {
|
||||
const method = req.method ?? 'GET';
|
||||
handlerByMethod[method](req, res);
|
||||
};
|
||||
}
|
||||
return getWebExportsHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add `duplex: 'half'` by default to all requests
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { addHelpers } from './helpers.js';
|
||||
import { createServer } from 'http';
|
||||
import { serializeBody } from '../utils.js';
|
||||
import {
|
||||
WAIT_UNTIL_TIMEOUT_MS,
|
||||
waitUntilWarning,
|
||||
serializeBody,
|
||||
} from '../utils.js';
|
||||
import { type Dispatcher, Headers, request as undiciRequest } from 'undici';
|
||||
import { listen } from 'async-listen';
|
||||
import { isAbsolute } from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { buildToHeaders } from '@edge-runtime/node-utils';
|
||||
import { promisify } from 'util';
|
||||
import type { ServerResponse, IncomingMessage } from 'http';
|
||||
import type { VercelProxyResponse } from '../types.js';
|
||||
import type { VercelRequest, VercelResponse } from './helpers.js';
|
||||
import type { Readable } from 'stream';
|
||||
import { Awaiter } from '../awaiter.js';
|
||||
|
||||
// @ts-expect-error
|
||||
const toHeaders = buildToHeaders({ Headers });
|
||||
@@ -43,14 +49,13 @@ async function createServerlessServer(
|
||||
const server = createServer(userCode);
|
||||
return {
|
||||
url: await listen(server, { host: '127.0.0.1', port: 0 }),
|
||||
onExit: async () => {
|
||||
server.close();
|
||||
},
|
||||
onExit: promisify(server.close.bind(server)),
|
||||
};
|
||||
}
|
||||
|
||||
async function compileUserCode(
|
||||
entrypointPath: string,
|
||||
awaiter: Awaiter,
|
||||
options: ServerlessServerOptions
|
||||
) {
|
||||
const id = isAbsolute(entrypointPath)
|
||||
@@ -71,7 +76,8 @@ async function compileUserCode(
|
||||
'Node.js v18 or above is required to use HTTP method exports in your functions.'
|
||||
);
|
||||
}
|
||||
const { getWebExportsHandler } = await import('./helpers-web.js');
|
||||
const { createWebExportsHandler } = await import('./helpers-web.js');
|
||||
const getWebExportsHandler = createWebExportsHandler(awaiter);
|
||||
return getWebExportsHandler(listener, HTTP_METHODS);
|
||||
}
|
||||
|
||||
@@ -88,7 +94,19 @@ export async function createServerlessEventHandler(
|
||||
handler: (request: IncomingMessage) => Promise<VercelProxyResponse>;
|
||||
onExit: () => Promise<void>;
|
||||
}> {
|
||||
const userCode = await compileUserCode(entrypointPath, options);
|
||||
const awaiter = new Awaiter();
|
||||
|
||||
Object.defineProperty(globalThis, Symbol.for('@vercel/request-context'), {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
value: {
|
||||
get: () => ({
|
||||
waitUntil: awaiter.waitUntil.bind(awaiter),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const userCode = await compileUserCode(entrypointPath, awaiter, options);
|
||||
const server = await createServerlessServer(userCode);
|
||||
const isStreaming = options.mode === 'streaming';
|
||||
|
||||
@@ -122,8 +140,21 @@ export async function createServerlessEventHandler(
|
||||
};
|
||||
};
|
||||
|
||||
const onExit = () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn(waitUntilWarning(entrypointPath));
|
||||
resolve();
|
||||
}, WAIT_UNTIL_TIMEOUT_MS);
|
||||
|
||||
Promise.all([awaiter.awaiting(), server.onExit()])
|
||||
.then(() => resolve())
|
||||
.catch(reject)
|
||||
.finally(() => clearTimeout(timeout));
|
||||
});
|
||||
|
||||
return {
|
||||
handler,
|
||||
onExit: server.onExit,
|
||||
onExit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@ import { pathToRegexp } from 'path-to-regexp';
|
||||
import type { IncomingMessage } from 'http';
|
||||
import { extname } from 'path';
|
||||
|
||||
// When exiting this process, wait for Vercel Function server to finish
|
||||
// all its work, especially waitUntil promises before exiting this process.
|
||||
//
|
||||
// Here we use a short timeout (10 seconds) to let the user know that
|
||||
// it has a long-running waitUntil promise.
|
||||
const WAIT_UNTIL_TIMEOUT = 10;
|
||||
export const WAIT_UNTIL_TIMEOUT_MS = 10 * 1000;
|
||||
|
||||
export const waitUntilWarning = (entrypointPath: string) =>
|
||||
`
|
||||
The function \`${entrypointPath
|
||||
.split('/')
|
||||
.pop()}\` is still running after ${WAIT_UNTIL_TIMEOUT}s.
|
||||
(hint: do you have a long-running waitUntil() promise?)
|
||||
`.trim();
|
||||
|
||||
export function getRegExpFromMatchers(matcherOrMatchers: unknown): string {
|
||||
if (!matcherOrMatchers) {
|
||||
return '^/.*$';
|
||||
|
||||
12
packages/node/test/dev-fixtures/wait-until-ctx-edge.js
Normal file
12
packages/node/test/dev-fixtures/wait-until-ctx-edge.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const baseUrl = ({ headers }) =>
|
||||
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
|
||||
|
||||
export function GET(request, ctx) {
|
||||
const { searchParams } = new URL(request.url, baseUrl(request));
|
||||
const url = searchParams.get('url');
|
||||
|
||||
ctx.waitUntil(fetch(url));
|
||||
return Response.json({ keys: Object.keys(ctx) });
|
||||
}
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
@@ -0,0 +1,12 @@
|
||||
function lazyError() {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('oh no'));
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
export function GET(_, ctx) {
|
||||
ctx.waitUntil(lazyError());
|
||||
return Response.json({ keys: Object.keys(ctx) });
|
||||
}
|
||||
10
packages/node/test/dev-fixtures/wait-until-ctx-node.js
Normal file
10
packages/node/test/dev-fixtures/wait-until-ctx-node.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const baseUrl = ({ headers }) =>
|
||||
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
|
||||
|
||||
export function GET(request, ctx) {
|
||||
const { searchParams } = new URL(request.url, baseUrl(request));
|
||||
const url = searchParams.get('url');
|
||||
|
||||
ctx.waitUntil(fetch(url));
|
||||
return Response.json({ keys: Object.keys(ctx) });
|
||||
}
|
||||
13
packages/node/test/dev-fixtures/wait-until-edge.js
Normal file
13
packages/node/test/dev-fixtures/wait-until-edge.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { waitUntil } from '@vercel/functions';
|
||||
|
||||
const baseUrl = ({ headers }) =>
|
||||
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
|
||||
|
||||
export function GET(request) {
|
||||
const { searchParams } = new URL(request.url, baseUrl(request));
|
||||
const url = searchParams.get('url');
|
||||
waitUntil(fetch(url));
|
||||
return new Response('OK');
|
||||
}
|
||||
|
||||
export const config = { runtime: 'edge' };
|
||||
11
packages/node/test/dev-fixtures/wait-until-node.js
Normal file
11
packages/node/test/dev-fixtures/wait-until-node.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { waitUntil } from '@vercel/functions';
|
||||
|
||||
const baseUrl = ({ headers }) =>
|
||||
`${headers.get('x-forwarded-proto')}://${headers.get('x-forwarded-host')}`;
|
||||
|
||||
export function GET(request) {
|
||||
const { searchParams } = new URL(request.url, baseUrl(request));
|
||||
const url = searchParams.get('url');
|
||||
waitUntil(fetch(url));
|
||||
return new Response('OK');
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { forkDevServer, readMessage } from '../../src/fork-dev-server';
|
||||
import { resolve, extname } from 'path';
|
||||
import { fetch } from 'undici';
|
||||
import { createServer } from 'http';
|
||||
import { listen } from 'async-listen';
|
||||
import zlib from 'zlib';
|
||||
import { once } from 'node:events';
|
||||
import { fetch } from 'undici';
|
||||
import { promisify } from 'util';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
jest.setTimeout(20 * 1000);
|
||||
|
||||
@@ -29,23 +31,97 @@ function testForkDevServer(entrypoint: string) {
|
||||
});
|
||||
}
|
||||
|
||||
(NODE_MAJOR < 18 ? test.skip : test)(
|
||||
'web handlers for node runtime',
|
||||
async () => {
|
||||
const child = testForkDevServer('./web-handlers-node.js');
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
const teardown: any = [];
|
||||
afterAll(() => Promise.all(teardown.map((fn: any) => fn())));
|
||||
|
||||
const { address, port } = result.value;
|
||||
async function withHttpServer(hander: (req: any, res: any) => void) {
|
||||
const server = createServer(hander);
|
||||
teardown.push(promisify(server.close.bind(server)));
|
||||
const address = await listen(server, { port: 0, host: '127.0.0.1' });
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
async function withDevServer(
|
||||
entrypoint: string,
|
||||
fn: (url: string) => Promise<void>,
|
||||
{ runningTimeout }: { runningTimeout?: number } = {}
|
||||
) {
|
||||
const child = testForkDevServer(entrypoint);
|
||||
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
const { address, port } = result.value;
|
||||
const url = `http://${address}:${port}`;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
return await fn(url);
|
||||
} finally {
|
||||
const elapsed = Date.now() - start;
|
||||
if (runningTimeout) await setTimeout(runningTimeout - elapsed);
|
||||
child.send('shutdown', error => error && child.kill(9));
|
||||
if (child.exitCode === null) await once(child, 'exit');
|
||||
}
|
||||
}
|
||||
|
||||
(NODE_MAJOR < 18 ? describe.skip : describe)('web handlers', () => {
|
||||
describe('for node runtime', () => {
|
||||
test('with `waitUntil` from import', () =>
|
||||
withDevServer(
|
||||
'./wait-until-node.js',
|
||||
async (url: string) => {
|
||||
let isWaitUntilCalled = false;
|
||||
const serverUrl = await withHttpServer((_, res) => {
|
||||
isWaitUntilCalled = true;
|
||||
res.end();
|
||||
});
|
||||
await fetch(`${url}/api/wait-until-node?url=${serverUrl}`);
|
||||
await setTimeout(50); // wait a bit for waitUntil resolution
|
||||
expect(isWaitUntilCalled).toBe(true);
|
||||
},
|
||||
{ runningTimeout: 300 }
|
||||
));
|
||||
|
||||
test('with `waitUntil` from context', () =>
|
||||
withDevServer(
|
||||
'./wait-until-ctx-node.js',
|
||||
async (url: string) => {
|
||||
let isWaitUntilCalled = false;
|
||||
const serverUrl = await withHttpServer((_, res) => {
|
||||
isWaitUntilCalled = true;
|
||||
res.end();
|
||||
});
|
||||
const response = await fetch(
|
||||
`${url}/api/wait-until-ctx-node?url=${serverUrl}`
|
||||
);
|
||||
expect(await response.json()).toEqual({ keys: ['waitUntil'] });
|
||||
await setTimeout(50); // wait a bit for waitUntil resolution
|
||||
expect(isWaitUntilCalled).toBe(true);
|
||||
},
|
||||
{ runningTimeout: 300 }
|
||||
));
|
||||
|
||||
test('with `waitUntil` from context rejecting a promise ', () =>
|
||||
withDevServer(
|
||||
'./wait-until-ctx-node-rejected.js',
|
||||
async (url: string) => {
|
||||
const response = await fetch(
|
||||
`${url}/api/wait-until-ctx-node-rejected`
|
||||
);
|
||||
await setTimeout(100); // wait a bit for waitUntil resolution
|
||||
expect(response.status).toBe(200);
|
||||
},
|
||||
{ runningTimeout: 100 }
|
||||
));
|
||||
|
||||
test('exporting GET', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'GET',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -57,12 +133,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using GET',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting POST', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'POST',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -74,12 +151,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using POST',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting DELETE', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -91,12 +169,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using DELETE',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'PUT' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting PUT', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -108,12 +187,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using PUT',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting PATCH', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -125,25 +205,31 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using PATCH',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'HEAD' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting HEAD', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'HEAD',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
transferEncoding: response.headers.get('transfer-encoding'),
|
||||
'x-web-handler': response.headers.get('x-web-handler'),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: '',
|
||||
transferEncoding: null,
|
||||
'x-web-handler': 'Web handler using HEAD',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-node`,
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting OPTIONS', () =>
|
||||
withDevServer('./web-handlers-node.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'OPTIONS',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -155,30 +241,140 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using OPTIONS',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
}
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
(NODE_MAJOR < 18 ? test.skip : test)(
|
||||
'web handlers for edge runtime',
|
||||
async () => {
|
||||
const child = testForkDevServer('./web-handlers-edge.js');
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
describe('for edge runtime', () => {
|
||||
test('with `waitUntil` from import', () =>
|
||||
withDevServer(
|
||||
'./wait-until-edge.js',
|
||||
async (url: string) => {
|
||||
let isWaitUntilCalled = false;
|
||||
const serverUrl = await withHttpServer((_, res) => {
|
||||
isWaitUntilCalled = true;
|
||||
res.end();
|
||||
});
|
||||
await fetch(`${url}/api/wait-until-edge?url=${serverUrl}`);
|
||||
await setTimeout(50); // wait a bit for waitUntil resolution
|
||||
expect(isWaitUntilCalled).toBe(true);
|
||||
},
|
||||
{ runningTimeout: 300 }
|
||||
));
|
||||
|
||||
const { address, port } = result.value;
|
||||
test('with `waitUntil` from context', () =>
|
||||
withDevServer(
|
||||
'./wait-until-ctx-edge.js',
|
||||
async (url: string) => {
|
||||
let isWaitUntilCalled = false;
|
||||
const serverUrl = await withHttpServer((_, res) => {
|
||||
isWaitUntilCalled = true;
|
||||
res.end();
|
||||
});
|
||||
const response = await fetch(
|
||||
`${url}/api/wait-until-ctx-edge?url=${serverUrl}`
|
||||
);
|
||||
expect(await response.json()).toEqual({ keys: ['waitUntil'] });
|
||||
await setTimeout(50); // wait a bit for waitUntil resolution
|
||||
expect(isWaitUntilCalled).toBe(true);
|
||||
},
|
||||
{ runningTimeout: 300 }
|
||||
));
|
||||
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
test("user code doesn't interfere with runtime", () =>
|
||||
withDevServer('./edge-self.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/edge-self`);
|
||||
expect({
|
||||
status: response.status,
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
});
|
||||
}));
|
||||
|
||||
test('with `WebSocket`', () =>
|
||||
withDevServer('./edge-websocket.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/edge-websocket`);
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: '3210',
|
||||
});
|
||||
}));
|
||||
|
||||
test('with `Buffer`', () =>
|
||||
withDevServer('./edge-buffer.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/edge-buffer`);
|
||||
expect({
|
||||
status: response.status,
|
||||
json: await response.json(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
json: {
|
||||
encoded: Buffer.from('Hello, world!').toString('base64'),
|
||||
'Buffer === B.Buffer': true,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
test('runs a mjs endpoint', () =>
|
||||
withDevServer('./esm-module.mjs', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/hello`);
|
||||
expect({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
headers: expect.objectContaining({
|
||||
'x-hello': 'world',
|
||||
}),
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
}));
|
||||
(process.platform === 'win32' ? test.skip : test)(
|
||||
'runs a esm typescript endpoint',
|
||||
() =>
|
||||
withDevServer('./esm-module.ts', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/hello`);
|
||||
expect({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
headers: expect.objectContaining({
|
||||
'x-hello': 'world',
|
||||
}),
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
})
|
||||
);
|
||||
(process.platform === 'win32' ? test.skip : test)(
|
||||
'allow setting multiple cookies with same name',
|
||||
() =>
|
||||
withDevServer('./multiple-cookies.ts', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/hello`, { method: 'GET' });
|
||||
expect({
|
||||
status: response.status,
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
expect(response.headers.getSetCookie()).toEqual([
|
||||
'a=x',
|
||||
'b=y',
|
||||
'c=z',
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
test('exporting GET', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'GET',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -190,12 +386,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using GET',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting POST', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'POST',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -207,29 +404,31 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using POST',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
}));
|
||||
|
||||
console.log(response);
|
||||
test('exporting DELETE', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
transferEncoding: response.headers.get('transfer-encoding'),
|
||||
'x-web-handler': response.headers.get('x-web-handler'),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: 'Web handler using DELETE',
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using DELETE',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'PUT' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting PUT', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -241,12 +440,13 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using PUT',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'PATCH' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting PATCH', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'PATCH',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -258,25 +458,31 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using PATCH',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'HEAD' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting HEAD', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'HEAD',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
transferEncoding: response.headers.get('transfer-encoding'),
|
||||
'x-web-handler': response.headers.get('x-web-handler'),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: '',
|
||||
transferEncoding: null,
|
||||
'x-web-handler': 'Web handler using HEAD',
|
||||
});
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/web-handlers-edge`,
|
||||
{ method: 'OPTIONS' }
|
||||
);
|
||||
}));
|
||||
|
||||
test('exporting OPTIONS', () =>
|
||||
withDevServer('./web-handlers-edge.js', async (url: string) => {
|
||||
const response = await fetch(`${url}/api/web-handlers-node`, {
|
||||
method: 'OPTIONS',
|
||||
});
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
@@ -288,338 +494,6 @@ function testForkDevServer(entrypoint: string) {
|
||||
transferEncoding: 'chunked',
|
||||
'x-web-handler': 'Web handler using OPTIONS',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
(NODE_MAJOR < 18 ? test.skip : test)(
|
||||
'buffer fetch response correctly',
|
||||
async () => {
|
||||
const child = testForkDevServer('./serverless-fetch.js');
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.setHeader('Content-Encoding', 'br');
|
||||
const searchParams = new URLSearchParams(req.url!.substring(1));
|
||||
const encoding = searchParams.get('encoding') ?? 'identity';
|
||||
res.writeHead(200, {
|
||||
'content-type': 'text/plain',
|
||||
'content-encoding': encoding,
|
||||
});
|
||||
let payload = Buffer.from('Greetings, Vercel');
|
||||
|
||||
if (encoding === 'br') {
|
||||
payload = zlib.brotliCompressSync(payload, {
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: 0,
|
||||
},
|
||||
});
|
||||
} else if (encoding === 'gzip') {
|
||||
payload = zlib.gzipSync(payload, {
|
||||
level: zlib.constants.Z_BEST_SPEED,
|
||||
});
|
||||
} else if (encoding === 'deflate') {
|
||||
payload = zlib.deflateSync(payload, {
|
||||
level: zlib.constants.Z_BEST_SPEED,
|
||||
});
|
||||
}
|
||||
|
||||
res.end(payload);
|
||||
});
|
||||
|
||||
const serverUrl = (await listen(server)).toString();
|
||||
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=br`
|
||||
);
|
||||
expect(response.headers.get('content-encoding')).toBe('br');
|
||||
expect(response.headers.get('content-length')).toBe('21');
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=gzip`
|
||||
);
|
||||
expect(response.headers.get('content-encoding')).toBe('gzip');
|
||||
expect(response.headers.get('content-length')).toBe('37');
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
|
||||
}
|
||||
{
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/serverless-fetch?url=${serverUrl}&encoding=deflate`
|
||||
);
|
||||
expect(response.headers.get('content-encoding')).toBe('deflate');
|
||||
expect(response.headers.get('content-length')).toBe('25');
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({ status: 200, body: 'Greetings, Vercel' });
|
||||
}
|
||||
} finally {
|
||||
server.close();
|
||||
child.kill(9);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
test("user code doesn't interfere with runtime", async () => {
|
||||
const child = testForkDevServer('./edge-self.js');
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(`http://${address}:${port}/api/edge-self`);
|
||||
|
||||
expect({
|
||||
status: response.status,
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
});
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('runs an edge function that uses `WebSocket`', async () => {
|
||||
const child = testForkDevServer('./edge-websocket.js');
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/edge-websocket`
|
||||
);
|
||||
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: '3210',
|
||||
});
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('runs an edge function that uses `buffer`', async () => {
|
||||
const child = testForkDevServer('./edge-buffer.js');
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(`http://${address}:${port}/api/edge-buffer`);
|
||||
expect({
|
||||
status: response.status,
|
||||
json: await response.json(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
json: {
|
||||
encoded: Buffer.from('Hello, world!').toString('base64'),
|
||||
'Buffer === B.Buffer': true,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('runs a mjs endpoint', async () => {
|
||||
const child = testForkDevServer('./esm-module.mjs');
|
||||
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(`http://${address}:${port}/api/hello`);
|
||||
expect({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
headers: expect.objectContaining({
|
||||
'x-hello': 'world',
|
||||
}),
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('runs a esm typescript endpoint', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('Skipping test on Windows');
|
||||
return;
|
||||
}
|
||||
|
||||
const child = testForkDevServer('./esm-module.ts');
|
||||
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(`http://${address}:${port}/api/hello`);
|
||||
expect({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers),
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
headers: expect.objectContaining({
|
||||
'x-hello': 'world',
|
||||
}),
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('allow setting multiple cookies with same name', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('Skipping test on Windows');
|
||||
return;
|
||||
}
|
||||
|
||||
const child = testForkDevServer('./multiple-cookies.ts');
|
||||
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error(`Exited. error: ${JSON.stringify(result.value)}`);
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(`http://${address}:${port}/api/hello`);
|
||||
expect({
|
||||
status: response.status,
|
||||
text: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
text: 'Hello, world!',
|
||||
});
|
||||
|
||||
expect(response.headers.getSetCookie()).toEqual(['a=x', 'b=y', 'c=z']);
|
||||
} finally {
|
||||
child.kill(9);
|
||||
}
|
||||
});
|
||||
|
||||
test('dev server waits for waitUntil promises to resolve', async () => {
|
||||
async function startPingServer() {
|
||||
let resolve: (value: any) => void;
|
||||
const promise = new Promise<void>(resolve_ => {
|
||||
resolve = resolve_;
|
||||
});
|
||||
|
||||
const pingServer = createServer((req, res) => {
|
||||
res.end('pong');
|
||||
resolve('got a fetch from waitUntil');
|
||||
});
|
||||
|
||||
const pingUrl = (await listen(pingServer)).toString();
|
||||
return {
|
||||
pingUrl,
|
||||
pingServer,
|
||||
promise,
|
||||
};
|
||||
}
|
||||
|
||||
async function withTimeout(
|
||||
promise: Promise<unknown>,
|
||||
name: string,
|
||||
ms: number
|
||||
) {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise(resolve =>
|
||||
setTimeout(
|
||||
() => resolve(`${name} promise was not resolved in ${ms} ms`),
|
||||
ms
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
const { promise: pingPromise, pingServer, pingUrl } = await startPingServer();
|
||||
const child = testForkDevServer('./edge-waituntil.js');
|
||||
const exitPromise = new Promise(resolve => {
|
||||
child.on('exit', code => {
|
||||
resolve(`child has exited with ${code}`);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await readMessage(child);
|
||||
if (result.state !== 'message') {
|
||||
throw new Error('Exited. error: ' + JSON.stringify(result.value));
|
||||
}
|
||||
|
||||
const { address, port } = result.value;
|
||||
const response = await fetch(
|
||||
`http://${address}:${port}/api/edge-waituntil`,
|
||||
{
|
||||
headers: {
|
||||
'x-ping-url': pingUrl,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect({
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}).toEqual({
|
||||
status: 200,
|
||||
body: 'running waitUntil promises asynchronously...',
|
||||
});
|
||||
|
||||
// Dev server should keep running until waitUntil promise resolves...
|
||||
child.send('shutdown');
|
||||
|
||||
// Wait for waitUntil promise to resolve...
|
||||
expect(await withTimeout(pingPromise, 'ping server', 3000)).toBe(
|
||||
'got a fetch from waitUntil'
|
||||
);
|
||||
// Make sure child process has exited.
|
||||
expect(await withTimeout(exitPromise, 'child exit', 5000)).toBe(
|
||||
'child has exited with 0'
|
||||
);
|
||||
} finally {
|
||||
child.kill(9);
|
||||
pingServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @vercel/static-build
|
||||
|
||||
## 2.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Support Ruby and Python static site generators in AL2023 build image ([#11529](https://github.com/vercel/vercel/pull/11529))
|
||||
|
||||
## 2.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Revert `SUPPORTED_RUBY_VERSION` change to fix Ruby site generators on AL2 build image ([#11522](https://github.com/vercel/vercel/pull/11522))
|
||||
|
||||
## 2.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/static-build",
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.3",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/build-step",
|
||||
|
||||
@@ -4,7 +4,12 @@ import fetch from 'node-fetch';
|
||||
import getPort from 'get-port';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import frameworks, { Framework } from '@vercel/frameworks';
|
||||
import { spawn, type ChildProcess, type SpawnOptions } from 'child_process';
|
||||
import {
|
||||
spawn,
|
||||
spawnSync,
|
||||
type ChildProcess,
|
||||
type SpawnOptions,
|
||||
} from 'child_process';
|
||||
import { existsSync, readFileSync, statSync, readdirSync, mkdirSync } from 'fs';
|
||||
import { cpus } from 'os';
|
||||
import {
|
||||
@@ -48,7 +53,6 @@ import {
|
||||
import { getHugoUrl } from './utils/hugo';
|
||||
import { once } from 'events';
|
||||
|
||||
const SUPPORTED_RUBY_VERSION = '3.3.0';
|
||||
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
|
||||
|
||||
const DEV_SERVER_PORT_BIND_TIMEOUT = ms('5m');
|
||||
@@ -579,9 +583,13 @@ export const build: BuildV2 = async ({
|
||||
pathList.push(vendorBin); // Add `./vendor/bin`
|
||||
debug(`Added "${vendorBin}" to PATH env because a Gemfile was found`);
|
||||
const dir = path.join(workPath, 'vendor', 'bundle', 'ruby');
|
||||
const rubyVersion = SUPPORTED_RUBY_VERSION;
|
||||
const rubyVersion = spawnSync(
|
||||
'ruby',
|
||||
['-e', 'print "#{ RUBY_VERSION }"'],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
if (rubyVersion) {
|
||||
gemHome = path.join(dir, rubyVersion);
|
||||
gemHome = path.join(dir, rubyVersion.stdout.trim());
|
||||
debug(`Set GEM_HOME="${gemHome}" because a Gemfile was found`);
|
||||
}
|
||||
}
|
||||
|
||||
6721
packages/static-build/test/fixtures/hydrogen-v2023/package-lock.json
generated
vendored
6721
packages/static-build/test/fixtures/hydrogen-v2023/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,9 @@
|
||||
"prettier": "@shopify/prettier-config",
|
||||
"dependencies": {
|
||||
"@remix-run/react": "1.19.1",
|
||||
"@shopify/cli": "3.59.1",
|
||||
"@shopify/cli-hydrogen": "^5.5.2",
|
||||
"@shopify/hydrogen": "^2023.10.6",
|
||||
"@shopify/cli": "3.49.2",
|
||||
"@shopify/cli-hydrogen": "^5.4.1",
|
||||
"@shopify/hydrogen": "^2023.7.9",
|
||||
"@shopify/remix-oxygen": "^1.1.5",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
||||
58
packages/static-build/test/fixtures/ionic-angular-v8/package-lock.json
generated
vendored
58
packages/static-build/test/fixtures/ionic-angular-v8/package-lock.json
generated
vendored
@@ -14,7 +14,7 @@
|
||||
"@angular/core": "^16.0.0",
|
||||
"@angular/platform-browser": "^16.0.0",
|
||||
"@angular/platform-browser-dynamic": "^16.2.12",
|
||||
"@ionic/angular": "^8.0.0",
|
||||
"@ionic/angular": "^8.1.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.13.3"
|
||||
@@ -3000,11 +3000,11 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@ionic/angular": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.0.1.tgz",
|
||||
"integrity": "sha512-VqGsntNXPpDemzfPv2U28fgP5+DTcElatmQfcHANlG61f4t6/QJ/1IweNph0I4gZddieZwW3LF5ag4BAmGRhzw==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.1.0.tgz",
|
||||
"integrity": "sha512-/cl5+C9WW+8dH2Sfi92SBFpxGAubRhVJC2TnsNcJYBmWw9fUDV7DfrUplXshopDoqouOFMYvelbvmrqigGyxwg==",
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.0.1",
|
||||
"@ionic/core": "8.1.0",
|
||||
"ionicons": "^7.0.0",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -3119,11 +3119,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.0.1.tgz",
|
||||
"integrity": "sha512-6FEa0kSXSs82aCYcB7JcLGt5Z0XBU8mRFQGVrJtdh3ybQQntIAWKHc9H2OFaiT3SSAK+XQqlU6kq0jM9nWKveQ==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-CTa0JZA7T0Je7HiAinj/kjpxChQYDvitFBqMtNv88nOJn1KerbUKmV2JfQ0quNFneN8z/bBdNOaKc8T++cyDyg==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.17.1",
|
||||
"@stencil/core": "^4.17.2",
|
||||
"ionicons": "^7.2.2",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -3924,9 +3924,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.1.tgz",
|
||||
"integrity": "sha512-nlARe1QtK5abnCG8kPQKJMWiELg39vKabvf3ebm6YEhQA35CgrxC1pVYTsYq3yktJKoY+k+VzGRnATLKyaLbvA==",
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.2.tgz",
|
||||
"integrity": "sha512-MX7yaLmpTU9iZvCire9nhecTcE0qBlV0vPWrLMeIXewYN7/hb8B3NjnhQyBKC93FDPI8NBRmt6KIugLw9zcRZg==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@@ -8171,9 +8171,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.3.1.tgz",
|
||||
"integrity": "sha512-1boG4EQTBBpQ4/0PU60Yi78Iw/k8iNtKu9c0NmsbzHGnWAcwpiovG9Wi/rk5UlF+DC+CR4XDCxKo91YqvAxkww==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
@@ -15842,11 +15842,11 @@
|
||||
"peer": true
|
||||
},
|
||||
"@ionic/angular": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.0.1.tgz",
|
||||
"integrity": "sha512-VqGsntNXPpDemzfPv2U28fgP5+DTcElatmQfcHANlG61f4t6/QJ/1IweNph0I4gZddieZwW3LF5ag4BAmGRhzw==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.1.0.tgz",
|
||||
"integrity": "sha512-/cl5+C9WW+8dH2Sfi92SBFpxGAubRhVJC2TnsNcJYBmWw9fUDV7DfrUplXshopDoqouOFMYvelbvmrqigGyxwg==",
|
||||
"requires": {
|
||||
"@ionic/core": "8.0.1",
|
||||
"@ionic/core": "8.1.0",
|
||||
"ionicons": "^7.0.0",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -15927,11 +15927,11 @@
|
||||
}
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.0.1.tgz",
|
||||
"integrity": "sha512-6FEa0kSXSs82aCYcB7JcLGt5Z0XBU8mRFQGVrJtdh3ybQQntIAWKHc9H2OFaiT3SSAK+XQqlU6kq0jM9nWKveQ==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-CTa0JZA7T0Je7HiAinj/kjpxChQYDvitFBqMtNv88nOJn1KerbUKmV2JfQ0quNFneN8z/bBdNOaKc8T++cyDyg==",
|
||||
"requires": {
|
||||
"@stencil/core": "^4.17.1",
|
||||
"@stencil/core": "^4.17.2",
|
||||
"ionicons": "^7.2.2",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -16481,9 +16481,9 @@
|
||||
}
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.1.tgz",
|
||||
"integrity": "sha512-nlARe1QtK5abnCG8kPQKJMWiELg39vKabvf3ebm6YEhQA35CgrxC1pVYTsYq3yktJKoY+k+VzGRnATLKyaLbvA=="
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.17.2.tgz",
|
||||
"integrity": "sha512-MX7yaLmpTU9iZvCire9nhecTcE0qBlV0vPWrLMeIXewYN7/hb8B3NjnhQyBKC93FDPI8NBRmt6KIugLw9zcRZg=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
@@ -19760,9 +19760,9 @@
|
||||
}
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.3.1.tgz",
|
||||
"integrity": "sha512-1boG4EQTBBpQ4/0PU60Yi78Iw/k8iNtKu9c0NmsbzHGnWAcwpiovG9Wi/rk5UlF+DC+CR4XDCxKo91YqvAxkww==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
|
||||
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
|
||||
"requires": {
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@angular/core": "^16.0.0",
|
||||
"@angular/platform-browser": "^16.0.0",
|
||||
"@angular/platform-browser-dynamic": "^16.2.12",
|
||||
"@ionic/angular": "^8.0.0",
|
||||
"@ionic/angular": "^8.1.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.13.3"
|
||||
|
||||
3
packages/static-build/test/fixtures/jekyll-v4-al2/.gitignore
vendored
Normal file
3
packages/static-build/test/fixtures/jekyll-v4-al2/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
_site
|
||||
.sass-cache
|
||||
.jekyll-metadata
|
||||
24
packages/static-build/test/fixtures/jekyll-v4-al2/404.html
vendored
Normal file
24
packages/static-build/test/fixtures/jekyll-v4-al2/404.html
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
.container {
|
||||
margin: 10px auto;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 30px 0;
|
||||
font-size: 4em;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
|
||||
<p><strong>Page not found :(</strong></p>
|
||||
<p>The requested page could not be found.</p>
|
||||
</div>
|
||||
34
packages/static-build/test/fixtures/jekyll-v4-al2/Gemfile
vendored
Normal file
34
packages/static-build/test/fixtures/jekyll-v4-al2/Gemfile
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Hello! This is where you manage which Jekyll version is used to run.
|
||||
# When you want to use a different version, change it below, save the
|
||||
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
|
||||
#
|
||||
# bundle exec jekyll serve
|
||||
#
|
||||
# This will help ensure the proper Jekyll version is running.
|
||||
# Happy Jekylling!
|
||||
gem "jekyll", "~> 4.0"
|
||||
|
||||
# This is the default theme for new Jekyll sites. You may change this to anything you like.
|
||||
gem "minima", "~> 2.0"
|
||||
|
||||
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
|
||||
# uncomment the line below. To upgrade, run `bundle update github-pages`.
|
||||
# gem "github-pages", group: :jekyll_plugins
|
||||
|
||||
# If you have any plugins, put them here!
|
||||
group :jekyll_plugins do
|
||||
gem "jekyll-feed", "~> 0.6"
|
||||
end
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
# and associated library.
|
||||
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
|
||||
gem "tzinfo", "~> 1.2"
|
||||
gem "tzinfo-data"
|
||||
end
|
||||
|
||||
# Performance-booster for watching directories on Windows
|
||||
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
|
||||
|
||||
89
packages/static-build/test/fixtures/jekyll-v4-al2/Gemfile.lock
vendored
Normal file
89
packages/static-build/test/fixtures/jekyll-v4-al2/Gemfile.lock
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.2.2)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0)
|
||||
eventmachine (1.2.7)
|
||||
ffi (1.16.3)
|
||||
forwardable-extended (2.6.0)
|
||||
google-protobuf (3.24.4-arm64-darwin)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (4.3.2)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 1.0)
|
||||
jekyll-sass-converter (>= 2.0, < 4.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (~> 2.3, >= 2.3.1)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
liquid (~> 4.0)
|
||||
mercenary (>= 0.3.6, < 0.5)
|
||||
pathutil (~> 0.9)
|
||||
rouge (>= 3.0, < 5.0)
|
||||
safe_yaml (~> 1.0)
|
||||
terminal-table (>= 1.8, < 4.0)
|
||||
webrick (~> 1.7)
|
||||
jekyll-feed (0.17.0)
|
||||
jekyll (>= 3.7, < 5.0)
|
||||
jekyll-sass-converter (3.0.0)
|
||||
sass-embedded (~> 1.54)
|
||||
jekyll-seo-tag (2.8.0)
|
||||
jekyll (>= 3.8, < 5.0)
|
||||
jekyll-watch (2.2.1)
|
||||
listen (~> 3.0)
|
||||
kramdown (2.4.0)
|
||||
rexml
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.4)
|
||||
listen (3.8.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.4.0)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (5.0.3)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.6)
|
||||
rouge (4.1.3)
|
||||
safe_yaml (1.0.5)
|
||||
sass-embedded (1.68.0-arm64-darwin)
|
||||
google-protobuf (~> 3.23)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.11)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2023.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
unicode-display_width (2.5.0)
|
||||
wdm (0.1.1)
|
||||
webrick (1.8.1)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
|
||||
DEPENDENCIES
|
||||
jekyll (~> 4.0)
|
||||
jekyll-feed (~> 0.6)
|
||||
minima (~> 2.0)
|
||||
tzinfo (~> 1.2)
|
||||
tzinfo-data
|
||||
wdm (~> 0.1.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.19
|
||||
45
packages/static-build/test/fixtures/jekyll-v4-al2/_config.yml
vendored
Normal file
45
packages/static-build/test/fixtures/jekyll-v4-al2/_config.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely edit after that. If you find
|
||||
# yourself editing this file very often, consider using Jekyll's data files
|
||||
# feature for the data you need to update frequently.
|
||||
#
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site settings
|
||||
# These are used to personalize your new site. If you look in the HTML files,
|
||||
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
||||
# You can create any custom variable you would like, and they will be accessible
|
||||
# in the templates via {{ site.myvariable }}.
|
||||
title: Your awesome title
|
||||
email: your-email@example.com
|
||||
description: >- # this means to ignore newlines until "baseurl:"
|
||||
Write an awesome description for your new site here. You can edit this
|
||||
line in _config.yml. It will appear in your document head meta (for
|
||||
Google search results) and in your feed.xml site description.
|
||||
baseurl: "" # the subpath of your site, e.g. /blog
|
||||
url: "" # the base hostname & protocol for your site, e.g. http://example.com
|
||||
twitter_username: jekyllrb
|
||||
github_username: jekyll
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
theme: minima
|
||||
plugins:
|
||||
- jekyll-feed
|
||||
sass:
|
||||
quiet_deps: true
|
||||
|
||||
# Exclude from processing.
|
||||
# The following items will not be processed, by default. Create a custom list
|
||||
# to override the default setting.
|
||||
# exclude:
|
||||
# - Gemfile
|
||||
# - Gemfile.lock
|
||||
# - node_modules
|
||||
# - vendor/bundle/
|
||||
# - vendor/cache/
|
||||
# - vendor/gems/
|
||||
# - vendor/ruby/
|
||||
10
packages/static-build/test/fixtures/jekyll-v4-al2/probes.json
vendored
Normal file
10
packages/static-build/test/fixtures/jekyll-v4-al2/probes.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "Your awesome title" },
|
||||
{ "path": "/about/", "mustContain": "You can find out more info" },
|
||||
{
|
||||
"path": "/jekyll/update/2019/09/06/welcome-to-jekyll.html",
|
||||
"mustContain": "Go ahead and edit it"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
_site
|
||||
.sass-cache
|
||||
.jekyll-cache
|
||||
.jekyll-metadata
|
||||
vendor
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
permalink: /404.html
|
||||
layout: default
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Hello! This is where you manage which Jekyll version is used to run.
|
||||
# When you want to use a different version, change it below, save the
|
||||
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
|
||||
@@ -8,27 +7,27 @@ source "https://rubygems.org"
|
||||
#
|
||||
# This will help ensure the proper Jekyll version is running.
|
||||
# Happy Jekylling!
|
||||
gem "jekyll", "~> 4.0"
|
||||
|
||||
gem "jekyll", "~> 4.3.3"
|
||||
# This is the default theme for new Jekyll sites. You may change this to anything you like.
|
||||
gem "minima", "~> 2.0"
|
||||
|
||||
gem "minima", "~> 2.5"
|
||||
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
|
||||
# uncomment the line below. To upgrade, run `bundle update github-pages`.
|
||||
# gem "github-pages", group: :jekyll_plugins
|
||||
|
||||
# If you have any plugins, put them here!
|
||||
group :jekyll_plugins do
|
||||
gem "jekyll-feed", "~> 0.6"
|
||||
gem "jekyll-feed", "~> 0.12"
|
||||
end
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
# and associated library.
|
||||
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
|
||||
gem "tzinfo", "~> 1.2"
|
||||
platforms :mingw, :x64_mingw, :mswin, :jruby do
|
||||
gem "tzinfo", ">= 1", "< 3"
|
||||
gem "tzinfo-data"
|
||||
end
|
||||
|
||||
# Performance-booster for watching directories on Windows
|
||||
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
|
||||
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
|
||||
|
||||
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
|
||||
# do not have a Java counterpart.
|
||||
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.5)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.2.3)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0)
|
||||
eventmachine (1.2.7)
|
||||
ffi (1.16.3)
|
||||
forwardable-extended (2.6.0)
|
||||
google-protobuf (3.24.4-arm64-darwin)
|
||||
google-protobuf (4.26.1)
|
||||
rake (>= 13)
|
||||
google-protobuf (4.26.1-aarch64-linux)
|
||||
rake (>= 13)
|
||||
google-protobuf (4.26.1-arm64-darwin)
|
||||
rake (>= 13)
|
||||
google-protobuf (4.26.1-x86-linux)
|
||||
rake (>= 13)
|
||||
google-protobuf (4.26.1-x86_64-darwin)
|
||||
rake (>= 13)
|
||||
google-protobuf (4.26.1-x86_64-linux)
|
||||
rake (>= 13)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (4.3.2)
|
||||
jekyll (4.3.3)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
em-websocket (~> 0.5)
|
||||
@@ -44,7 +55,7 @@ GEM
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.4)
|
||||
listen (3.8.0)
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.4.0)
|
||||
@@ -54,36 +65,99 @@ GEM
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (5.0.3)
|
||||
public_suffix (5.0.5)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.6)
|
||||
rouge (4.1.3)
|
||||
rouge (4.2.1)
|
||||
safe_yaml (1.0.5)
|
||||
sass-embedded (1.68.0-arm64-darwin)
|
||||
google-protobuf (~> 3.23)
|
||||
sass-embedded (1.76.0)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
rake (>= 13.0.0)
|
||||
sass-embedded (1.76.0-aarch64-linux-android)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-aarch64-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-aarch64-linux-musl)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-aarch64-mingw-ucrt)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-arm-linux-androideabi)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-arm-linux-gnueabihf)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-arm-linux-musleabihf)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-arm64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-riscv64-linux-android)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-riscv64-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-riscv64-linux-musl)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86-cygwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86-linux-android)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86-linux-musl)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86-mingw-ucrt)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86_64-cygwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86_64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86_64-linux-android)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86_64-linux-gnu)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
sass-embedded (1.76.0-x86_64-linux-musl)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.11)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2023.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
unicode-display_width (2.5.0)
|
||||
wdm (0.1.1)
|
||||
webrick (1.8.1)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
aarch64-linux
|
||||
aarch64-linux-android
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
aarch64-mingw-ucrt
|
||||
arm-linux-androideabi
|
||||
arm-linux-gnueabihf
|
||||
arm-linux-musleabihf
|
||||
arm64-darwin
|
||||
riscv64-linux-android
|
||||
riscv64-linux-gnu
|
||||
riscv64-linux-musl
|
||||
ruby
|
||||
x86-cygwin
|
||||
x86-linux
|
||||
x86-linux-android
|
||||
x86-linux-gnu
|
||||
x86-linux-musl
|
||||
x86-mingw-ucrt
|
||||
x86_64-cygwin
|
||||
x86_64-darwin
|
||||
x86_64-linux
|
||||
x86_64-linux-android
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
jekyll (~> 4.0)
|
||||
jekyll-feed (~> 0.6)
|
||||
minima (~> 2.0)
|
||||
tzinfo (~> 1.2)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
jekyll (~> 4.3.3)
|
||||
jekyll-feed (~> 0.12)
|
||||
minima (~> 2.5)
|
||||
tzinfo (>= 1, < 3)
|
||||
tzinfo-data
|
||||
wdm (~> 0.1.0)
|
||||
wdm (~> 0.1.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.19
|
||||
2.5.9
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
#
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
#
|
||||
# If you need help with YAML syntax, here are some quick references for you:
|
||||
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
|
||||
# https://learnxinyminutes.com/docs/yaml/
|
||||
#
|
||||
# Site settings
|
||||
# These are used to personalize your new site. If you look in the HTML files,
|
||||
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
||||
# You can create any custom variable you would like, and they will be accessible
|
||||
# in the templates via {{ site.myvariable }}.
|
||||
|
||||
title: Your awesome title
|
||||
email: your-email@example.com
|
||||
description: >- # this means to ignore newlines until "baseurl:"
|
||||
@@ -25,20 +30,25 @@ twitter_username: jekyllrb
|
||||
github_username: jekyll
|
||||
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
theme: minima
|
||||
plugins:
|
||||
- jekyll-feed
|
||||
sass:
|
||||
quiet_deps: true
|
||||
|
||||
# Exclude from processing.
|
||||
# The following items will not be processed, by default. Create a custom list
|
||||
# to override the default setting.
|
||||
# The following items will not be processed, by default.
|
||||
# Any item listed under the `exclude:` key here will be automatically added to
|
||||
# the internal "default list".
|
||||
#
|
||||
# Excluded items can be processed by explicitly listing the directories or
|
||||
# their entries' file path in the `include:` list.
|
||||
#
|
||||
# exclude:
|
||||
# - .sass-cache/
|
||||
# - .jekyll-cache/
|
||||
# - gemfiles/
|
||||
# - Gemfile
|
||||
# - Gemfile.lock
|
||||
# - node_modules
|
||||
# - node_modules/
|
||||
# - vendor/bundle/
|
||||
# - vendor/cache/
|
||||
# - vendor/gems/
|
||||
|
||||
29
packages/static-build/test/fixtures/jekyll-v4/_posts/2024-05-01-welcome-to-jekyll.markdown
vendored
Normal file
29
packages/static-build/test/fixtures/jekyll-v4/_posts/2024-05-01-welcome-to-jekyll.markdown
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: post
|
||||
title: "Welcome to Jekyll!"
|
||||
date: 2024-05-01 20:47:19 -0700
|
||||
categories: jekyll update
|
||||
---
|
||||
You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.
|
||||
|
||||
Jekyll requires blog post files to be named according to the following format:
|
||||
|
||||
`YEAR-MONTH-DAY-title.MARKUP`
|
||||
|
||||
Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works.
|
||||
|
||||
Jekyll also offers powerful support for code snippets:
|
||||
|
||||
{% highlight ruby %}
|
||||
def print_hi(name)
|
||||
puts "Hi, #{name}"
|
||||
end
|
||||
print_hi('Tom')
|
||||
#=> prints 'Hi, Tom' to STDOUT.
|
||||
{% endhighlight %}
|
||||
|
||||
Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk].
|
||||
|
||||
[jekyll-docs]: https://jekyllrb.com/docs/home
|
||||
[jekyll-gh]: https://github.com/jekyll/jekyll
|
||||
[jekyll-talk]: https://talk.jekyllrb.com/
|
||||
18
packages/static-build/test/fixtures/jekyll-v4/about.markdown
vendored
Normal file
18
packages/static-build/test/fixtures/jekyll-v4/about.markdown
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
layout: page
|
||||
title: About
|
||||
permalink: /about/
|
||||
---
|
||||
|
||||
This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/)
|
||||
|
||||
You can find the source code for Minima at GitHub:
|
||||
[jekyll][jekyll-organization] /
|
||||
[minima](https://github.com/jekyll/minima)
|
||||
|
||||
You can find the source code for Jekyll at GitHub:
|
||||
[jekyll][jekyll-organization] /
|
||||
[jekyll](https://github.com/jekyll/jekyll)
|
||||
|
||||
|
||||
[jekyll-organization]: https://github.com/jekyll
|
||||
6
packages/static-build/test/fixtures/jekyll-v4/index.markdown
vendored
Normal file
6
packages/static-build/test/fixtures/jekyll-v4/index.markdown
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# Feel free to add content and custom Front Matter to this file.
|
||||
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
|
||||
|
||||
layout: home
|
||||
---
|
||||
@@ -3,7 +3,7 @@
|
||||
{ "path": "/", "mustContain": "Your awesome title" },
|
||||
{ "path": "/about/", "mustContain": "You can find out more info" },
|
||||
{
|
||||
"path": "/jekyll/update/2019/09/06/welcome-to-jekyll.html",
|
||||
"path": "/jekyll/update/2024/05/02/welcome-to-jekyll.html",
|
||||
"mustContain": "Go ahead and edit it"
|
||||
}
|
||||
]
|
||||
|
||||
5
packages/static-build/test/fixtures/middleman-v4-al2/.gitignore
vendored
Normal file
5
packages/static-build/test/fixtures/middleman-v4-al2/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.bundle
|
||||
.cache
|
||||
.DS_Store
|
||||
.sass-cache
|
||||
build/
|
||||
6
packages/static-build/test/fixtures/middleman-v4-al2/Gemfile
vendored
Normal file
6
packages/static-build/test/fixtures/middleman-v4-al2/Gemfile
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'middleman', '~> 4.2'
|
||||
gem 'middleman-autoprefixer', '~> 2.7'
|
||||
gem 'tzinfo-data', platforms: [:mswin, :mingw, :jruby, :x64_mingw]
|
||||
gem 'wdm', '~> 0.1', platforms: [:mswin, :mingw, :x64_mingw]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user