Skip to content
Draft
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
24 changes: 24 additions & 0 deletions apps/docs/__tests__/LandingPageInstallInstructions_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
open Vitest

test("install instructions update their class prop and keep copying available", async () => {
let screen = await render(<LandingPageInstallInstructions className="initial-layout" />)
let root = switch document->WebAPI.Document.querySelector(".initial-layout") {
| Value(root) => root
| Null => failwith("expected the install instructions root")
}
await element(root)->toHaveAttribute("class", "w-full max-w-400 initial-layout")

await screen->rerender(<LandingPageInstallInstructions className="updated-layout" />)

await element(root)->toHaveAttribute("class", "w-full max-w-400 updated-layout")
let template = await screen->getByLabelText("Copy npx create-rescript-app command")
await element(template)->toBeVisible

await screen->rerender(<LandingPageInstallInstructions />)

await element(root)->toHaveAttribute("class", "w-full max-w-400 ")
let copy = await screen->getByLabelText("Copy npm install rescript command")
await copy->click
let feedback = await screen->getByText("Copied!")
await element(feedback)->toBeVisible
})
6 changes: 5 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"check:algolia-public-env": "node _scripts/LogAlgoliaEnvStatus.mjs",
"build": "yarn build:res && yarn build:scripts && yarn check:algolia-public-env && yarn build:update-index && yarn build:vite && yarn build:generate-sitemap",
"ci:format": "cd ../.. && oxfmt --check",
"ci:homepage-performance": "node scripts/homepage-performance.mjs",
"ci:homepage-performance": "node --test scripts/__tests__/homepage-compiler.test.mjs && node scripts/homepage-performance.mjs",
"ci:test": "vitest --run --browser.headless",
"ci:test:e2e": "cypress run --config-file cypress.homepage.config.mjs --browser chrome",
"ci:test:scripts": "vitest run --config vitest.scripts.config.mjs",
Expand Down Expand Up @@ -78,14 +78,17 @@
"vfile-matter": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.7",
"@react-router/dev": "^8.3.1",
"@responsive-image/core": "2.1.0",
"@responsive-image/vite-plugin": "3.0.1",
"@rolldown/plugin-babel": "^0.2.4",
"@tailwindcss/vite": "^4.3.0",
"@types/react": "^19.2.14",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser-playwright": "^5.0.0",
"auto-image-converter": "^2.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"chokidar": "^4.0.3",
"cypress": "^15.13.1",
"cypress-real-events": "^1.15.1",
Expand All @@ -96,6 +99,7 @@
"oxfmt": "^0.46.0",
"playwright": "^1.63.0",
"remark-cli": "^12.0.1",
"rolldown": "1.2.8",
"search-insights": "^2.17.3",
"tailwindcss": "^4",
"to-vfile": "^8.0.0",
Expand Down
258 changes: 258 additions & 0 deletions apps/docs/scripts/__tests__/homepage-compiler.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import assert from "node:assert/strict";
import { readFile, readdir } from "node:fs/promises";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { parseSync, transformAsync, traverse, types } from "@babel/core";
import { homepageCompilerOptions } from "../../vite-react-compiler.mjs";

const optedInComponents = [
{
name: "LandingPageIntro",
directory: "src/components",
compiledNames: ["LandingPageIntro"],
},
{
name: "LandingPageInstallInstructions",
directory: "src/components",
compiledNames: ["LandingPageInstallInstructions"],
},
{
name: "LandingPageTrustedBy",
directory: "src/components",
compiledNames: ["LandingPageTrustedBy"],
},
{
name: "NavbarPrimary",
directory: "src/components",
compiledNames: [
"NavbarPrimary",
"NavbarPrimary$LeftContent",
"NavbarPrimary$RightContent",
],
},
];
const optedInNames = optedInComponents.flatMap(
({ compiledNames }) => compiledNames,
);

async function transformComponent({ name, directory }) {
const filename = fileURLToPath(
new URL(`../../${directory}/${name}.jsx`, import.meta.url),
);
const result = await transformAsync(await readFile(filename, "utf8"), {
filename,
ast: true,
code: false,
babelrc: false,
configFile: false,
parserOpts: { plugins: ["jsx"] },
presets: homepageCompilerOptions().presets.map((preset) => preset.preset),
});
assert.ok(result?.ast, `${name} must produce a Babel AST`);
return result.ast;
}

function cacheBindings(ast) {
return ast.program.body.flatMap((node) => {
if (
!types.isImportDeclaration(node) ||
node.source.value !== "react/compiler-runtime"
) {
return [];
}
return node.specifiers.flatMap((specifier) =>
types.isImportSpecifier(specifier) &&
types.isIdentifier(specifier.imported, { name: "c" })
? [specifier.local.name]
: [],
);
});
}

function cachedComponentNames(ast) {
const bindings = cacheBindings(ast);
const names = new Set();
traverse(ast, {
CallExpression(path) {
if (
types.isIdentifier(path.node.callee) &&
bindings.includes(path.node.callee.name)
) {
const component = path.getFunctionParent();
assert.ok(component?.isFunctionDeclaration());
assert.ok(component.node.id);
names.add(component.node.id.name);
}
},
});
return [...names].sort();
}

function isMemoCacheSentinel(node) {
return (
types.isStringLiteral(node, { value: "react.memo_cache_sentinel" }) ||
(types.isTemplateLiteral(node) &&
node.expressions.length === 0 &&
node.quasis[0]?.value.cooked === "react.memo_cache_sentinel")
);
}

function memoizedFunctionCount(ast) {
const functions = new Set();
traverse(ast, {
CallExpression(path) {
const { callee, arguments: args } = path.node;
if (
types.isMemberExpression(callee, { computed: false }) &&
types.isIdentifier(callee.object, { name: "Symbol" }) &&
types.isIdentifier(callee.property, { name: "for" }) &&
args.length === 1 &&
isMemoCacheSentinel(args[0])
) {
const component = path.getFunctionParent();
assert.ok(component, "memoization must belong to a component");
functions.add(component.node);
}
},
});
return functions.size;
}

for (const component of optedInComponents) {
test(`${component.name} opts in to React 19 compiler memoization`, async () => {
const ast = await transformComponent(component);
assert.deepEqual(cachedComponentNames(ast), component.compiledNames);
assert.equal(memoizedFunctionCount(ast), component.compiledNames.length);
});
}

test("unannotated interactive homepage components remain uncompiled", async () => {
const ast = await transformComponent({
name: "LandingPageCopyButton",
directory: "src/components",
});
assert.deepEqual(cacheBindings(ast), []);
assert.deepEqual(cachedComponentNames(ast), []);
assert.equal(memoizedFunctionCount(ast), 0);
});

test("compiler file filtering includes only opted-in generated application modules", () => {
const { include, exclude } = homepageCompilerOptions();
const matches = (filename) =>
include.test(filename) &&
!exclude.some((pattern) => pattern.test(filename));
for (const filename of [
"/repo/apps/docs/app/routes/LandingPageIntro.jsx",
"/repo/apps/docs/app/routes/LandingPageTrustedBy.jsx?import",
"C:\\repo\\apps\\docs\\app\\routes\\LandingPageIntro.jsx",
"/repo/apps/docs/src/components/LandingPageIntro.jsx",
"/repo/apps/docs/src/components/LandingPageTrustedBy.jsx?import",
"C:\\repo\\apps\\docs\\src\\components\\LandingPageIntro.jsx",
"/repo/apps/docs/src/components/NavbarPrimary.jsx",
"/repo/apps/docs/src/components/NavbarPrimary.jsx?import",
"C:\\repo\\apps\\docs\\src\\components\\NavbarPrimary.jsx",
]) {
assert.equal(matches(filename), true, filename);
}
for (const filename of [
"/repo/apps/docs/app/routes/LandingPageIntro.res",
"/repo/apps/docs/app/routes/LandingPageIntro.mjs",
"/repo/apps/docs/app/routes/LandingPageIntro.jsx.map",
"/repo/apps/docs/app/routes/TryRoute.jsx",
"/repo/apps/guide/app/routes/LandingPageIntro.jsx",
"/repo/node_modules/example/apps/docs/app/routes/LandingPageIntro.jsx",
"C:\\repo\\node_modules\\example\\apps\\docs\\app\\routes\\LandingPageIntro.jsx",
"/repo/apps/docs/src/components/LandingPageIntro.res",
"/repo/apps/docs/src/components/LandingPageIntro.jsx.map",
"/repo/apps/docs/src/components/Search.jsx",
"/repo/apps/docs/src/components/NavbarSecondary.jsx",
"/repo/apps/guide/src/components/LandingPageIntro.jsx",
"/repo/node_modules/example/apps/docs/src/components/LandingPageIntro.jsx",
"\0rolldown/runtime.js",
]) {
assert.equal(matches(filename), false, filename);
}
});

test("the shared preset preserves annotation mode and excludes server compilation", () => {
const [{ preset, rolldown }] = homepageCompilerOptions().presets;
assert.deepEqual(preset().plugins, [
[
"babel-plugin-react-compiler",
{ compilationMode: "annotation", target: "19" },
],
]);
assert.equal(
rolldown.filter.code.test('function Example() { "use memo"; }'),
true,
);
assert.equal(rolldown.filter.code.test("function Example() {}"), false);
assert.equal(
rolldown.applyToEnvironmentHook({ config: { consumer: "client" } }),
true,
);
assert.equal(
rolldown.applyToEnvironmentHook({ config: { consumer: "server" } }),
false,
);
assert.deepEqual(rolldown.optimizeDeps.include, ["react/compiler-runtime"]);
});

test("the production homepage bundle contains its three compiled components", async () => {
const directory = new URL("../../build/client/assets/", import.meta.url);
const filenames = (await readdir(directory)).filter((filename) =>
/^LandingPageRoute-[^/]+\.js$/.test(filename),
);
assert.equal(filenames.length, 1, "the production homepage entry must exist");
const contents = await readFile(new URL(filenames[0], directory), "utf8");
const ast = parseSync(contents, { babelrc: false, configFile: false });
assert.ok(ast);
assert.equal(memoizedFunctionCount(ast), 3);
});

test("the production client bundle contains the compiled primary navbar", async () => {
const directory = new URL("../../build/client/assets/", import.meta.url);
const assets = await Promise.all(
(await readdir(directory))
.filter((filename) => filename.endsWith(".js"))
.map(async (filename) => ({
filename,
contents: await readFile(new URL(filename, directory), "utf8"),
})),
);
const matches = assets.filter(({ contents }) =>
contents.includes("navbar-primary-left-content"),
);
assert.equal(matches.length, 1, "the production primary navbar must exist");
const ast = parseSync(matches[0].contents, {
babelrc: false,
configFile: false,
});
assert.ok(ast);
assert.equal(memoizedFunctionCount(ast), 3);
});

test("the production server leaves the annotated components uncompiled", async () => {
const contents = await readFile(
new URL("../../build/server/index.js", import.meta.url),
"utf8",
);
const ast = parseSync(contents, { babelrc: false, configFile: false });
assert.ok(ast);
const annotatedComponents = [];
traverse(ast, {
FunctionDeclaration(path) {
if (
optedInNames.includes(path.node.id?.name) &&
path.node.body.directives.some(
(directive) => directive.value.value === "use memo",
)
) {
annotatedComponents.push(path.node.id.name);
}
},
});
assert.deepEqual(annotatedComponents.sort(), [...optedInNames].sort());
assert.deepEqual(cacheBindings(ast), []);
assert.equal(memoizedFunctionCount(ast), 0);
});
30 changes: 16 additions & 14 deletions apps/docs/src/components/LandingPageInstallInstructions.res
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ let copyBox = text => {
}

@react.component
let make = (~className="") => {
<div className={`w-full max-w-400 ${className}`}>
<h2 className="hl-3 lg:mt-12"> {React.string("Quick Install")} </h2>
<div className="captions text-gray-40 mb-2 mt-1">
{React.string(
"You can quickly add ReScript to your existing JavaScript codebase via npm / yarn:",
)}
let make =
@directive("'use memo'")
(~className="") => {
<div className={`w-full max-w-400 ${className}`}>
<h2 className="hl-3 lg:mt-12"> {React.string("Quick Install")} </h2>
<div className="captions text-gray-40 mb-2 mt-1">
{React.string(
"You can quickly add ReScript to your existing JavaScript codebase via npm / yarn:",
)}
</div>
{copyBox("npm install rescript")}
<div className="captions text-gray-40 mb-2 mt-2">
{React.string("Or generate a new project from the official template with npx:")}
</div>
{copyBox("npx create-rescript-app")}
</div>
{copyBox("npm install rescript")}
<div className="captions text-gray-40 mb-2 mt-2">
{React.string("Or generate a new project from the official template with npx:")}
</div>
{copyBox("npx create-rescript-app")}
</div>
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/** Opt in to React Compiler for the homepage browser build. */
@react.component
let make: (~className: string=?) => React.element
Loading
Loading