Compare commits

...

2 Commits

Author SHA1 Message Date
Steven
0cacb1bdac Publish Canary
- @vercel/build-utils@2.12.3-canary.22
 - vercel@23.1.3-canary.40
 - @vercel/client@10.2.3-canary.23
 - vercel-plugin-go@1.0.0-canary.7
 - vercel-plugin-node@1.12.2-canary.13
 - vercel-plugin-python@1.0.0-canary.8
 - vercel-plugin-ruby@1.0.0-canary.6
2021-11-24 18:12:26 -05:00
Steven
318bf35f82 [build-utils] Add support for writing routes-manifest.json (#7062)
* [build-utils] Add support for writing routes-manifest.json

* Add support for dynamicRoutes

* Add another test with multiple named params
2021-11-24 18:00:12 -05:00
21 changed files with 267 additions and 26 deletions

View File

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

View File

@@ -2,6 +2,7 @@ import fs from 'fs-extra';
import { join, dirname, relative } from 'path';
import glob from './fs/glob';
import { normalizePath } from './fs/normalize-path';
import { detectBuilders } from './detect-builders';
import { FILES_SYMBOL, getLambdaOptionsFromFunction, Lambda } from './lambda';
import type FileBlob from './file-blob';
import type { BuilderFunctions, BuildOptions, Files } from './types';
@@ -21,7 +22,12 @@ export function convertRuntimeToPlugin(
vercelConfig,
workPath,
}: {
vercelConfig: { functions?: BuilderFunctions; regions?: string[] };
vercelConfig: {
functions?: BuilderFunctions;
regions?: string[];
trailingSlash?: boolean;
cleanUrls?: boolean;
};
workPath: string;
}) {
const opts = { cwd: workPath };
@@ -29,7 +35,7 @@ export function convertRuntimeToPlugin(
delete files['vercel.json']; // Builders/Runtimes didn't have vercel.json
const entrypoints = await glob(`api/**/*${ext}`, opts);
const pages: { [key: string]: any } = {};
const { functions = {} } = vercelConfig;
const { functions = {}, cleanUrls, trailingSlash } = vercelConfig;
const traceDir = join(workPath, '.output', 'runtime-traced-files');
await fs.ensureDir(traceDir);
@@ -106,6 +112,61 @@ export function convertRuntimeToPlugin(
}
await updateFunctionsManifest({ vercelConfig, workPath, pages });
const {
warnings,
errors,
//defaultRoutes,
redirectRoutes,
//rewriteRoutes,
dynamicRoutesWithKeys,
// errorRoutes, already handled by pages404
} = await detectBuilders(Object.keys(files), null, {
tag: 'latest',
functions: functions,
projectSettings: undefined,
featHandleMiss: true,
cleanUrls,
trailingSlash,
});
if (errors) {
throw new Error(errors[0].message);
}
if (warnings) {
warnings.forEach(warning => console.warn(warning.message, warning.link));
}
const redirects = redirectRoutes
?.filter(r => r.src && 'headers' in r)
?.map(r => ({
source: r.src || '',
destination:
'headers' in r && r.headers?.Location ? r.headers.Location : '',
statusCode: 'status' in r && r.status ? r.status : 307,
regex: r.src || '',
}));
const dynamicRoutes = dynamicRoutesWithKeys?.map(r => {
const keys = Object.keys(r.routeKeys);
return {
page: '/' + r.fileName.slice(0, -ext.length),
regex: r.regex,
routeKeys: r.routeKeys,
namedRegex: r.regex
.split('([^/]+)')
.map((str, i) => str + (keys[i] ? `(?<${keys[i]}>[^/]+)` : ''))
.join(''),
};
});
await updateRoutesManifest({
workPath,
redirects,
rewrites: [],
dynamicRoutes,
});
};
}
@@ -172,32 +233,81 @@ export async function updateFunctionsManifest({
}
/**
* Will append routes to the `routes-manifest.json` file.
* If the file does not exist, it'll be created.
* Append routes to the `routes-manifest.json` file.
* If the file does not exist, it will be created.
*/
export async function updateRoutesManifest({
workPath,
redirects,
rewrites,
headers,
dynamicRoutes,
staticRoutes,
}: {
workPath: string;
redirects?: {
source: string;
destination: string;
statusCode: number;
regex: string;
}[];
rewrites?: {
source: string;
destination: string;
regex: string;
}[];
headers?: {
source: string;
headers: {
key: string;
value: string;
}[];
regex: string;
}[];
dynamicRoutes?: {
page: string;
regex: string;
namedRegex?: string;
routeKeys?: { [named: string]: string };
}[];
staticRoutes?: {
page: string;
regex: string;
namedRegex?: string;
routeKeys?: { [named: string]: string };
}[];
}) {
const routesManifestPath = join(workPath, '.output', 'routes-manifest.json');
const routesManifest = await readJson(routesManifestPath);
if (!routesManifest.version) routesManifest.version = 1;
if (!routesManifest.version) routesManifest.version = 3;
if (routesManifest.pages404 === undefined) routesManifest.pages404 = true;
if (redirects) {
if (!routesManifest.redirects) routesManifest.redirects = [];
routesManifest.redirects.push(...redirects);
}
if (rewrites) {
if (!routesManifest.rewrites) routesManifest.rewrites = [];
routesManifest.rewrites.push(...rewrites);
}
if (headers) {
if (!routesManifest.headers) routesManifest.headers = [];
routesManifest.headers.push(...headers);
}
if (dynamicRoutes) {
if (!routesManifest.dynamicRoutes) routesManifest.dynamicRoutes = [];
routesManifest.dynamicRoutes.push(...dynamicRoutes);
}
if (staticRoutes) {
if (!routesManifest.staticRoutes) routesManifest.staticRoutes = [];
routesManifest.staticRoutes.push(...staticRoutes);
}
await fs.writeFile(routesManifestPath, JSON.stringify(routesManifest));
}

View File

@@ -16,6 +16,12 @@ interface ErrorResponse {
link?: string;
}
interface DynamicRoutesWithKeys {
fileName: string;
regex: string;
routeKeys: { [key: string]: string };
}
interface Options {
tag?: 'canary' | 'latest' | string;
functions?: BuilderFunctions;
@@ -96,6 +102,7 @@ export async function detectBuilders(
redirectRoutes: Route[] | null;
rewriteRoutes: Route[] | null;
errorRoutes: Route[] | null;
dynamicRoutesWithKeys: DynamicRoutesWithKeys[] | null;
}> {
const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = [];
@@ -114,6 +121,7 @@ export async function detectBuilders(
redirectRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
dynamicRoutesWithKeys: null,
};
}
@@ -157,13 +165,14 @@ export async function detectBuilders(
const apiRoutes: Source[] = [];
const dynamicRoutes: Source[] = [];
const dynamicRoutesWithKeys: DynamicRoutesWithKeys[] = [];
// API
for (const fileName of sortedFiles) {
const apiBuilder = maybeGetApiBuilder(fileName, apiMatches, options);
if (apiBuilder) {
const { routeError, apiRoute, isDynamic } = getApiRoute(
const { routeError, apiRoute, isDynamic, routeKeys } = getApiRoute(
fileName,
apiSortedFiles,
options,
@@ -179,6 +188,7 @@ export async function detectBuilders(
redirectRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
dynamicRoutesWithKeys: null,
};
}
@@ -186,6 +196,11 @@ export async function detectBuilders(
apiRoutes.push(apiRoute);
if (isDynamic) {
dynamicRoutes.push(apiRoute);
dynamicRoutesWithKeys.push({
fileName,
regex: apiRoute.src,
routeKeys,
});
}
}
@@ -257,6 +272,7 @@ export async function detectBuilders(
defaultRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
dynamicRoutesWithKeys: null,
};
}
@@ -299,6 +315,7 @@ export async function detectBuilders(
defaultRoutes: null,
rewriteRoutes: null,
errorRoutes: null,
dynamicRoutesWithKeys: null,
};
}
@@ -342,6 +359,7 @@ export async function detectBuilders(
defaultRoutes: routesResult.defaultRoutes,
rewriteRoutes: routesResult.rewriteRoutes,
errorRoutes: routesResult.errorRoutes,
dynamicRoutesWithKeys,
};
}
@@ -675,6 +693,7 @@ function getApiRoute(
): {
apiRoute: Source | null;
isDynamic: boolean;
routeKeys: { [key: string]: string };
routeError: ErrorResponse | null;
} {
const conflictingSegment = getConflictingSegment(fileName);
@@ -683,6 +702,7 @@ function getApiRoute(
return {
apiRoute: null,
isDynamic: false,
routeKeys: {},
routeError: {
code: 'conflicting_path_segment',
message:
@@ -703,6 +723,7 @@ function getApiRoute(
return {
apiRoute: null,
isDynamic: false,
routeKeys: {},
routeError: {
code: 'conflicting_file_path',
message:
@@ -722,6 +743,7 @@ function getApiRoute(
return {
apiRoute: out.route,
isDynamic: out.isDynamic,
routeKeys: out.routeKeys,
routeError: null,
};
}
@@ -867,11 +889,12 @@ function createRouteFromPath(
filePath: string,
featHandleMiss: boolean,
cleanUrls: boolean
): { route: Source; isDynamic: boolean } {
): { route: Source; isDynamic: boolean; routeKeys: { [key: string]: string } } {
const parts = filePath.split('/');
let counter = 1;
const query: string[] = [];
const routeKeys: { [key: string]: string } = {};
let isDynamic = false;
const srcParts = parts.map((segment, i): string => {
@@ -881,6 +904,7 @@ function createRouteFromPath(
if (name !== null) {
// We can't use `URLSearchParams` because `$` would get escaped
query.push(`${name}=$${counter++}`);
routeKeys[name] = name;
isDynamic = true;
return `([^/]+)`;
} else if (isLast) {
@@ -929,7 +953,7 @@ function createRouteFromPath(
};
}
return { route, isDynamic };
return { route, isDynamic, routeKeys };
}
function getRouteResult(

View File

@@ -0,0 +1 @@
# users.rb

View File

@@ -1,9 +1,9 @@
{
"functions": {
"api/users/post.py": {
"api/users.rb": {
"memory": 3008
},
"api/not-matching-anything.py": {
"api/doesnt-exist.rb": {
"memory": 768
}
}

View File

@@ -0,0 +1 @@
# [id].py

View File

@@ -0,0 +1 @@
# project/[aid]/[bid]/index.py

View File

@@ -0,0 +1,7 @@
{
"functions": {
"api/users/post.py": {
"memory": 3008
}
}
}

View File

@@ -18,14 +18,52 @@ async function fsToJson(dir: string, output: Record<string, any> = {}) {
return output;
}
const workPath = join(__dirname, 'walk', 'python-api');
const invalidFuncWorkpath = join(
__dirname,
'convert-runtime',
'invalid-functions'
);
const pythonApiWorkpath = join(__dirname, 'convert-runtime', 'python-api');
describe('convert-runtime-to-plugin', () => {
afterEach(async () => {
await fs.remove(join(workPath, '.output'));
await fs.remove(join(invalidFuncWorkpath, '.output'));
await fs.remove(join(pythonApiWorkpath, '.output'));
});
it('should error with invalid functions prop', async () => {
const workPath = invalidFuncWorkpath;
const lambdaOptions = {
handler: 'index.handler',
runtime: 'nodejs14.x',
memory: 512,
maxDuration: 5,
environment: {},
regions: ['sfo1'],
};
const buildRuntime = async (opts: BuildOptions) => {
const lambda = await createLambda({
files: opts.files,
...lambdaOptions,
});
return { output: lambda };
};
const lambdaFiles = await fsToJson(workPath);
const vercelConfig = JSON.parse(lambdaFiles['vercel.json']);
delete lambdaFiles['vercel.json'];
const build = await convertRuntimeToPlugin(buildRuntime, '.js');
expect(build({ vercelConfig, workPath })).rejects.toThrow(
new Error(
'The pattern "api/doesnt-exist.rb" defined in `functions` doesn\'t match any Serverless Functions inside the `api` directory.'
)
);
});
it('should create correct fileystem for python', async () => {
const workPath = pythonApiWorkpath;
const lambdaOptions = {
handler: 'index.handler',
runtime: 'python3.9',
@@ -53,6 +91,7 @@ describe('convert-runtime-to-plugin', () => {
const output = await fsToJson(join(workPath, '.output'));
expect(output).toMatchObject({
'functions-manifest.json': expect.stringContaining('{'),
'routes-manifest.json': expect.stringContaining('{'),
'runtime-traced-files': lambdaFiles,
server: {
pages: {
@@ -80,14 +119,54 @@ describe('convert-runtime-to-plugin', () => {
},
});
const routesManifest = JSON.parse(output['routes-manifest.json']);
expect(routesManifest).toMatchObject({
version: 3,
pages404: true,
redirects: [],
rewrites: [
/* TODO: `handle: miss`
{
source: "^/api/(.+)(?:\\.(?:py))$",
destination: "/api/db/[id]?id=$1",
regex: "^/api/(.+)(?:\\.(?:py))$"
}
*/
],
dynamicRoutes: [
{
page: '/api/project/[aid]/[bid]/index',
regex: '^/api/project/([^/]+)/([^/]+)(/|/index|/index\\.py)?$',
routeKeys: { aid: 'aid', bid: 'bid' },
namedRegex:
'^/api/project/(?<aid>[^/]+)/(?<bid>[^/]+)(/|/index|/index\\.py)?$',
},
{
page: '/api/db/[id]',
regex: '^/api/db/([^/]+)$',
routeKeys: { id: 'id' },
namedRegex: '^/api/db/(?<id>[^/]+)$',
},
],
});
const indexJson = JSON.parse(output.server.pages.api['index.py.nft.json']);
expect(indexJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../runtime-traced-files/api/db/[id].py',
output: 'api/db/[id].py',
},
{
input: '../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input:
'../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: '../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',
@@ -117,10 +196,19 @@ describe('convert-runtime-to-plugin', () => {
expect(getJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/db/[id].py',
output: 'api/db/[id].py',
},
{
input: '../../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input:
'../../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',
@@ -150,10 +238,19 @@ describe('convert-runtime-to-plugin', () => {
expect(postJson).toMatchObject({
version: 1,
files: [
{
input: '../../../../../runtime-traced-files/api/db/[id].py',
output: 'api/db/[id].py',
},
{
input: '../../../../../runtime-traced-files/api/index.py',
output: 'api/index.py',
},
{
input:
'../../../../../runtime-traced-files/api/project/[aid]/[bid]/index.py',
output: 'api/project/[aid]/[bid]/index.py',
},
{
input: '../../../../../runtime-traced-files/api/users/get.py',
output: 'api/users/get.py',

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "23.1.3-canary.39",
"version": "23.1.3-canary.40",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -43,14 +43,14 @@
"node": ">= 12"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@vercel/go": "1.2.4-canary.4",
"@vercel/node": "1.12.2-canary.7",
"@vercel/python": "2.1.2-canary.0",
"@vercel/ruby": "1.2.8-canary.4",
"update-notifier": "4.1.0",
"vercel-plugin-middleware": "0.0.0-canary.7",
"vercel-plugin-node": "1.12.2-canary.12"
"vercel-plugin-node": "1.12.2-canary.13"
},
"devDependencies": {
"@next/env": "11.1.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "10.2.3-canary.22",
"version": "10.2.3-canary.23",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -40,7 +40,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vercel-plugin-go",
"version": "1.0.0-canary.6",
"version": "1.0.0-canary.7",
"main": "dist/index.js",
"license": "MIT",
"files": [
@@ -17,7 +17,7 @@
"prepublishOnly": "tsc"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@vercel/go": "1.2.4-canary.4"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "vercel-plugin-node",
"version": "1.12.2-canary.12",
"version": "1.12.2-canary.13",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -34,7 +34,7 @@
"@types/node-fetch": "2",
"@types/test-listen": "1.1.0",
"@types/yazl": "2.4.2",
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@vercel/fun": "1.0.3",
"@vercel/ncc": "0.24.0",
"@vercel/nft": "0.14.0",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vercel-plugin-python",
"version": "1.0.0-canary.7",
"version": "1.0.0-canary.8",
"main": "dist/index.js",
"license": "MIT",
"files": [
@@ -17,7 +17,7 @@
"prepublishOnly": "tsc"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@vercel/python": "2.1.2-canary.0"
},
"devDependencies": {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vercel-plugin-ruby",
"version": "1.0.0-canary.5",
"version": "1.0.0-canary.6",
"main": "dist/index.js",
"license": "MIT",
"files": [
@@ -17,7 +17,7 @@
"prepublishOnly": "tsc"
},
"dependencies": {
"@vercel/build-utils": "2.12.3-canary.21",
"@vercel/build-utils": "2.12.3-canary.22",
"@vercel/ruby": "1.2.8-canary.4"
},
"devDependencies": {