[remix] Add unit tests (#9469)

Moves parts of the `@vercel/remix` builder into util functions that have isolated unit tests. No functionality changes.
This commit is contained in:
Nathan Rajlich
2023-02-17 10:46:33 -08:00
committed by GitHub
parent 83ee5ea2b8
commit 63211b8b89
8 changed files with 306 additions and 40 deletions

View File

@@ -11,8 +11,9 @@
}, },
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",
"test-e2e": "pnpm test test/integration.test.ts", "test": "jest --env node --verbose --bail --runInBand",
"test": "jest --env node --verbose --bail --runInBand" "test-unit": "pnpm test test/unit.*test.*",
"test-e2e": "pnpm test test/integration.test.ts"
}, },
"files": [ "files": [
"dist", "dist",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

19
pnpm-lock.yaml generated
View File

@@ -1203,6 +1203,7 @@ packages:
semver: 6.3.0 semver: 6.3.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/@babel/core/7.5.0: /@babel/core/7.5.0:
resolution: {integrity: sha512-6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw==} resolution: {integrity: sha512-6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw==}
@@ -1268,7 +1269,7 @@ packages:
'@babel/core': ^7.0.0 '@babel/core': ^7.0.0
dependencies: dependencies:
'@babel/compat-data': 7.20.10 '@babel/compat-data': 7.20.10
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-validator-option': 7.18.6 '@babel/helper-validator-option': 7.18.6
browserslist: 4.21.4 browserslist: 4.21.4
lru-cache: 5.1.1 lru-cache: 5.1.1
@@ -1298,7 +1299,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0 '@babel/core': ^7.0.0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-annotate-as-pure': 7.18.6
regexpu-core: 5.2.2 regexpu-core: 5.2.2
@@ -1381,6 +1382,7 @@ packages:
'@babel/types': 7.20.7 '@babel/types': 7.20.7
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/@babel/helper-optimise-call-expression/7.18.6: /@babel/helper-optimise-call-expression/7.18.6:
resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
@@ -1506,6 +1508,7 @@ packages:
'@babel/types': 7.20.7 '@babel/types': 7.20.7
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/@babel/highlight/7.18.6: /@babel/highlight/7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
@@ -1675,7 +1678,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.20.12
@@ -1731,7 +1734,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.20.2
/@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.20.12: /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.20.12:
@@ -1850,7 +1853,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.20.2
/@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.12: /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.20.12:
@@ -1858,7 +1861,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.20.2
/@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.12: /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.20.12:
@@ -1980,7 +1983,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
dependencies: dependencies:
'@babel/core': 7.20.12_supports-color@7.2.0 '@babel/core': 7.20.12
'@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12 '@babel/helper-create-regexp-features-plugin': 7.20.5_@babel+core@7.20.12
'@babel/helper-plugin-utils': 7.20.2 '@babel/helper-plugin-utils': 7.20.2
@@ -2517,6 +2520,7 @@ packages:
globals: 11.12.0 globals: 11.12.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/@babel/types/7.20.7: /@babel/types/7.20.7:
resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==} resolution: {integrity: sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==}
@@ -10243,6 +10247,7 @@ packages:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
supports-color: 7.2.0 supports-color: 7.2.0
dev: true
/debuglog/1.0.1: /debuglog/1.0.1:
resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==}