Files
vercel/packages/frameworks/test/frameworks.unit.test.ts
Nathan Rajlich 1333071a3a Remix Vite plugin support (#11031)
Adds support for Remix apps which use the new Remix Vite plugin.

* The vanilla Remix + Vite template deploys correctly out-of-the-box,
however only a single Node.js function will be used, and a warning will
be printed saying to configure the `vercelPreset()` Preset.
* When used in conjunction with the `vercelPreset()` Preset
(https://github.com/vercel/remix/pull/81), allows for the application to
utilize Vercel-specific features, like per-route `export const config`
configuration, including multi-runtime (Node.js / Edge runtimes) within
the same app.

## To test this today

1. Generate a Remix + Vite project from the template:
    ```
    npx create-remix@latest --template remix-run/remix/templates/vite
    ```
1. Install `@vercel/remix`:
    ```
    npm i --save-dev @vercel/remix
    ```
1. **(Before Remix v2.8.0 is released)** - Update the `@remix-run/dev`
dependency to use the "pre" tag which contains [a bug
fix](https://github.com/remix-run/remix/pull/8864):
    ```
    npm i --save--dev @remix-run/dev@pre @remix-run/serve@pre
    ```
1. Configure the `vercelPreset()` in the `vite.config.ts` file:
    ```diff
    --- a/vite.config.ts
    +++ b/vite.config.ts
    @@ -1,10 +1,11 @@
     import { vitePlugin as remix } from "@remix-run/dev";
     import { installGlobals } from "@remix-run/node";
     import { defineConfig } from "vite";
    +import { vercelPreset } from "@vercel/remix/vite";
     import tsconfigPaths from "vite-tsconfig-paths";
    
     installGlobals();
    
     export default defineConfig({
    -  plugins: [remix(), tsconfigPaths()],
    +  plugins: [remix({ presets: [vercelPreset()] }), tsconfigPaths()],
     });
    ```
1. Create a new Vercel Project in the dashboard, and ensure the Vercel
preset is set to "Remix" in the Project Settings. The autodetection will
work correctly once this PR is merged, but for now it gets incorrectly
detected as "Vite" preset.
* **Hint**: You can create a new empty Project by running the `vercel
link` command.
<img width="545" alt="Screenshot 2024-02-27 at 10 37 11"
src="https://github.com/vercel/vercel/assets/71256/f46baf57-5d97-4bde-9529-c9165632cb30">
1. Deploy to Vercel, setting the `VERCEL_CLI_VERSION` environment
variable to use the changes in this PR:
    ```
vercel deploy -b
VERCEL_CLI_VERSION=https://vercel-git-tootallnate-zero-1217-research-remix-v-next-vite.vercel.sh/tarballs/vercel.tgz
    ```
2024-02-28 11:22:05 -06:00

324 lines
7.6 KiB
TypeScript
Vendored

import Ajv from 'ajv';
import assert from 'assert';
import { join } from 'path';
import { existsSync } from 'fs';
import { isString } from 'util';
import fetch from 'node-fetch';
import { URL, URLSearchParams } from 'url';
import frameworkList from '../src/frameworks';
// bump timeout for Windows as network can be slower
jest.setTimeout(15 * 1000);
const logoPrefix = 'https://api-frameworks.vercel.sh/framework-logos/';
const SchemaFrameworkDetectionItem = {
type: 'array',
items: [
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
path: {
type: 'string',
},
matchContent: {
type: 'string',
},
matchPackage: {
type: 'string',
},
},
},
],
};
const SchemaSettings = {
oneOf: [
{
type: 'object',
required: ['value'],
additionalProperties: false,
properties: {
value: {
type: ['string', 'null'],
},
placeholder: {
type: 'string',
},
},
},
{
type: 'object',
required: ['value', 'ignorePackageJsonScript'],
additionalProperties: false,
properties: {
value: {
type: 'string',
},
placeholder: {
type: 'string',
},
ignorePackageJsonScript: {
type: 'boolean',
},
},
},
{
type: 'object',
required: ['placeholder'],
additionalProperties: false,
properties: {
placeholder: {
type: 'string',
},
},
},
],
};
const RouteSchema = {
type: 'array',
items: {
properties: {
src: { type: 'string' },
dest: { type: 'string' },
status: { type: 'number' },
handle: { type: 'string' },
headers: { type: 'object' },
continue: { type: 'boolean' },
},
},
};
const Schema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: [
'name',
'slug',
'logo',
'description',
'settings',
'getOutputDirName',
],
properties: {
name: { type: 'string' },
slug: { type: ['string', 'null'] },
sort: { type: 'number' },
logo: { type: 'string' },
darkModeLogo: { type: 'string' },
screenshot: { type: 'string' },
demo: { type: 'string' },
tagline: { type: 'string' },
website: { type: 'string' },
description: { type: 'string' },
envPrefix: { type: 'string' },
useRuntime: {
type: 'object',
required: ['src', 'use'],
additionalProperties: false,
properties: {
src: { type: 'string' },
use: { type: 'string' },
},
},
ignoreRuntimes: {
type: 'array',
items: {
type: 'string',
},
},
detectors: {
type: 'object',
additionalProperties: false,
properties: {
every: SchemaFrameworkDetectionItem,
some: SchemaFrameworkDetectionItem,
},
},
settings: {
type: 'object',
required: [
'installCommand',
'buildCommand',
'devCommand',
'outputDirectory',
],
additionalProperties: false,
properties: {
installCommand: SchemaSettings,
buildCommand: SchemaSettings,
devCommand: SchemaSettings,
outputDirectory: SchemaSettings,
},
},
getOutputDirName: {
isFunction: true,
},
defaultRoutes: {
oneOf: [{ isFunction: true }, RouteSchema],
},
defaulHeaders: {
type: 'array',
items: {
properties: {
source: { type: 'string' },
regex: { type: 'string' },
headers: { type: 'object' },
continue: { type: 'boolean' },
},
},
},
disableRootMiddleware: {
type: 'boolean',
},
recommendedIntegrations: {
type: 'array',
items: {
type: 'object',
required: ['id', 'dependencies'],
additionalProperties: false,
properties: {
id: {
type: 'string',
},
dependencies: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
dependency: { type: 'string' },
cachePattern: { type: 'string' },
defaultVersion: { type: 'string' },
supersedes: { type: 'array', items: { type: 'string' } },
},
},
};
async function getDeployment(host: string) {
const query = new URLSearchParams();
query.set('url', host);
const res = await fetch(
`https://api.vercel.com/v11/deployments/get?${query}`
);
const body = await res.json();
return body;
}
describe('frameworks', () => {
it('ensure there is an example for every framework', async () => {
const root = join(__dirname, '..', '..', '..');
const getExample = (name: string) => join(root, 'examples', name);
const result = frameworkList
.map(f => f.slug)
.filter(isString)
.filter(f => existsSync(getExample(f)) === false);
expect(result).toEqual([]);
});
it('ensure schema', async () => {
const ajv = getValidator();
const result = ajv.validate(Schema, frameworkList);
if (ajv.errors) {
console.error(ajv.errors);
}
expect(result).toBe(true);
});
it('ensure logo starts with url prefix', async () => {
const invalid = frameworkList
.map(f => f.logo)
.filter(logo => {
return logo && !logo.startsWith(logoPrefix);
});
expect(invalid).toEqual([]);
});
it('ensure darkModeLogo starts with url prefix', async () => {
const invalid = frameworkList
.map(f => f.darkModeLogo)
.filter(darkModeLogo => {
return darkModeLogo && !darkModeLogo.startsWith(logoPrefix);
});
expect(invalid).toEqual([]);
});
it('ensure logo file exists in ./packages/frameworks/logos/', async () => {
const missing = frameworkList
.map(f => f.logo)
.filter(logo => {
const filename = logo.slice(logoPrefix.length);
const filepath = join(__dirname, '..', 'logos', filename);
return existsSync(filepath) === false;
});
expect(missing).toEqual([]);
});
it('ensure unique sort number', async () => {
const sortNumToSlug = new Map<number, string | null>();
frameworkList.forEach(f => {
if (f.sort) {
const duplicateSlug = sortNumToSlug.get(f.sort);
expect(duplicateSlug).toStrictEqual(undefined);
sortNumToSlug.set(f.sort, f.slug);
}
});
});
it('ensure unique slug', async () => {
const slugs = new Set<string>();
for (const { slug } of frameworkList) {
if (typeof slug === 'string') {
assert(!slugs.has(slug), `Slug "${slug}" is not unique`);
slugs.add(slug);
}
}
});
it('ensure all demo URLs are "public"', async () => {
await Promise.all(
frameworkList
.filter(f => typeof f.demo === 'string')
.map(async f => {
const url = new URL(f.demo!);
const deployment = await getDeployment(url.hostname);
assert.equal(
deployment.public,
true,
`Demo URL ${f.demo} is not "public"`
);
})
);
});
});
function getValidator() {
const ajv = new Ajv();
ajv.addKeyword('isFunction', {
compile: shouldMatch => data => {
const matches = typeof data === 'function';
return (shouldMatch && matches) || (!shouldMatch && !matches);
},
});
return ajv;
}