Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 257 additions & 130 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
- JackPriceBurns
- jacob-briscoe
- jacob-ebey
- jadlr
- JaffParker
- jakkku
- JakubDrozd
Expand Down
75 changes: 75 additions & 0 deletions integration/helpers/express.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import tsx from "dedent";

export function server() {
return tsx`
import { createRequestHandler } from "@react-router/express";
import express from "express";

const port = process.env.PORT ?? 3000
const hmrPort = process.env.HMR_PORT ?? 3001

const app = express();

const getLoadContext = () => ({});

if (process.env.NODE_ENV === "production") {
app.use(
"/assets",
express.static("build/client/assets", { immutable: true, maxAge: "1y" })
);
app.use(express.static("build/client", { maxAge: "1h" }));
app.all("*", createRequestHandler({
build: await import("./build/index.js"),
getLoadContext,
}));
} else {
const viteDevServer = await import("vite").then(
(vite) => vite.createServer({
server: {
middlewareMode: true,
hmr: { port: hmrPort },
},
})
);
app.use(viteDevServer.middlewares);
app.all("*", createRequestHandler({
build:() => viteDevServer.ssrLoadModule("virtual:react-router/server-build"),
getLoadContext,
}));
}

app.listen(port, () => console.log('http://localhost:' + port));
`;
}

export function rsc() {
return tsx`
import { createRequestListener } from "@mjackson/node-fetch-server";
import express from "express";

const port = process.env.PORT ?? 3000
const hmrPort = process.env.HMR_PORT ?? 3001

const app = express();

if (process.env.NODE_ENV === "production") {
app.use(
"/assets",
express.static("build/client/assets", { immutable: true, maxAge: "1y" })
);
app.all("*", createRequestListener((await import("./build/server/index.js")).default));
} else {
const viteDevServer = await import("vite").then(
(vite) => vite.createServer({
server: {
middlewareMode: true,
hmr: { port: hmrPort },
},
})
);
app.use(viteDevServer.middlewares);
}

app.listen(port, () => console.log('http://localhost:' + port));
`;
}
138 changes: 138 additions & 0 deletions integration/helpers/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ChildProcess } from "node:child_process";
import * as fs from "node:fs/promises";
import { fileURLToPath } from "node:url";

import { test as base } from "@playwright/test";
import {
execa,
ExecaError,
type Options,
parseCommandString,
type ResultPromise,
} from "execa";
import * as Path from "pathe";

import type { TemplateName } from "./vite.js";

declare module "@playwright/test" {
interface Page {
errors: Error[];
}
}

const __filename = fileURLToPath(import.meta.url);
const ROOT = Path.join(__filename, "../../..");
const TMP = Path.join(ROOT, ".tmp/integration");
const templatePath = (templateName: string) =>
Path.resolve(ROOT, "integration/helpers", templateName);

type Edits = Record<string, string | ((contents: string) => string)>;

async function applyEdits(cwd: string, edits: Edits) {
const promises = Object.entries(edits).map(async ([file, transform]) => {
const filepath = Path.join(cwd, file);
await fs.writeFile(
filepath,
typeof transform === "function"
? transform(await fs.readFile(filepath, "utf8"))
: transform,
"utf8",
);
return;
});
await Promise.all(promises);
}

export const test = base.extend<{
template: TemplateName;
files: Edits;
cwd: string;
edit: (edits: Edits) => Promise<void>;
$: (
command: string,
options?: Pick<Options, "env" | "timeout">,
) => ResultPromise<{ reject: false }> & {
buffer: { stdout: string; stderr: string };
};
}>({
template: ["vite-6-template", { option: true }],
files: [{}, { option: true }],
page: async ({ page }, use) => {
page.errors = [];
page.on("pageerror", (error: Error) => page.errors.push(error));
await use(page);
},

cwd: async ({ template, files }, use, testInfo) => {
await fs.mkdir(TMP, { recursive: true });
const cwd = await fs.mkdtemp(Path.join(TMP, template + "-"));
testInfo.attach("cwd", { body: cwd });

await fs.cp(templatePath(template), cwd, {
errorOnExist: true,
recursive: true,
});

await applyEdits(cwd, files);

await use(cwd);
},

edit: async ({ cwd }, use) => {
await use(async (edits) => applyEdits(cwd, edits));
},

$: async ({ cwd }, use) => {
const spawn = execa({
cwd,
env: {
NO_COLOR: "1",
FORCE_COLOR: "0",
},
reject: false,
});

let testHasEnded = false;
const processes: Array<ResultPromise> = [];
const unexpectedErrors: Array<Error> = [];

await use((command, options = {}) => {
const [file, ...args] = parseCommandString(command);

const p = spawn(file, args, options);
if (p instanceof ChildProcess) {
processes.push(p);
}

p.then((result) => {
if (!(result instanceof Error)) return result;

// Once the test has ended, this process will be killed as part of its teardown resulting in an ExecaError.
// We only care about surfacing errors that occurred during test execution, not during teardown.
const expectedError = testHasEnded && result instanceof ExecaError;
if (expectedError) return result;
unexpectedErrors.push(result);
});

const buffer = { stdout: "", stderr: "" };
p.stdout?.on("data", (data) => (buffer.stdout += data.toString()));
p.stderr?.on("data", (data) => (buffer.stderr += data.toString()));
return Object.assign(p, { buffer });
});

testHasEnded = true;
processes.forEach((p) => p.kill());

// Throw any unexpected errors that occurred during test execution
if (unexpectedErrors.length > 0) {
const errorMessage =
unexpectedErrors.length === 1
? `Unexpected process error: ${unexpectedErrors[0].message}`
: `${unexpectedErrors.length} unexpected process errors:\n${unexpectedErrors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`;

const error = new Error(errorMessage);
error.stack = unexpectedErrors[0].stack;
throw error;
}
},
});
29 changes: 29 additions & 0 deletions integration/helpers/stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Readable } from "node:stream";

export async function match(
stream: Readable,
pattern: string | RegExp,
options: {
/** Measured in ms */
timeout?: number;
} = {},
): Promise<RegExpMatchArray> {
// Prepare error outside of promise so that stacktrace points to caller of `matchLine`
const timeoutError = new Error(
`Timed out - Could not find pattern: ${pattern}`,
);
return new Promise(async (resolve, reject) => {
const timeout = setTimeout(
() => reject(timeoutError),
options.timeout ?? 10_000,
);
stream.on("data", (data) => {
const line: string = data.toString();
const matches = line.match(pattern);
if (matches) {
resolve(matches);
clearTimeout(timeout);
}
});
});
}
30 changes: 30 additions & 0 deletions integration/helpers/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const templates = [
// Vite Major templates
{ name: "vite-5-template", displayName: "Vite 5" },
{ name: "vite-6-template", displayName: "Vite 6" },
{ name: "vite-7-beta-template", displayName: "Vite 7 Beta" },
{ name: "vite-rolldown-template", displayName: "Vite Rolldown" },

// RSC templates
{ name: "rsc-vite", displayName: "RSC (Vite)" },
{ name: "rsc-parcel", displayName: "RSC (Parcel)" },
{ name: "rsc-vite-framework", displayName: "RSC Framework" },

// Cloudflare
// { name: "cloudflare-dev-proxy-template", displayName: "Cloudflare Dev Proxy" },
{ name: "vite-plugin-cloudflare-template", displayName: "Cloudflare" },
] as const;

export type Template = (typeof templates)[number];

export function getTemplates(names?: Array<Template["name"]>) {
if (names === undefined) return templates;
return templates.filter(({ name }) => names.includes(name));
}

export const viteMajorTemplates = getTemplates([
"vite-5-template",
"vite-6-template",
"vite-7-beta-template",
"vite-rolldown-template",
]);
2 changes: 1 addition & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"cheerio": "^1.0.0-rc.12",
"cross-spawn": "^7.0.3",
"dedent": "^0.7.0",
"execa": "^5.1.1",
"execa": "^9.6.0",
"express": "^4.19.2",
"get-port": "^5.1.1",
"glob": "8.0.3",
Expand Down
Loading