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
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"preview": "yarn build && static-server build/client",
"serve:build": "static-server --host 127.0.0.1 --port 4173 build/client",
"reanalyze": "rescript-tools reanalyze -all-cmt .",
"test": "node scripts/test.mjs",
"test": "yarn build:res && yarn build:scripts && yarn build:generate-llms && node scripts/test.mjs",
"test:e2e": "yarn build && bash ../../.github/scripts/homepage-e2e.sh",
"cy:open": "yarn build:res && cypress open --e2e --browser electron",
"cy:run": "yarn build:res && cypress run",
Expand Down
60 changes: 40 additions & 20 deletions apps/docs/public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,51 @@
/bucklescript-rebranding /blog/bucklescript-is-rebranding 308
/docs/guidelines/publishing-packages /docs/guides/publishing-packages 308

/docs/manual/v12.0.0/* /docs/manual/:splat 308
/docs/manual/next/* /docs/manual/:splat 308
/docs/react/latest/* /docs/react/:splat 308

# Legacy aliases serve the current deployment's generated manual text files.
/llms/manual/llms.txt /llms.txt 307
/llms/manual/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/latest/llms.txt /llms.txt 307
/llms/manual/latest/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/latest/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/next/llms.txt /llms.txt 307
/llms/manual/v13.0.0/llms.txt /llms/manual/v13/llms.txt 307
/llms/manual/v13.0.0/llms-full.txt /llms/manual/v13/llm-full.txt 307
/llms/manual/v13.0.0/llms-small.txt /llms/manual/v13/llm-small.txt 307
/llms/manual/v13.0.0/* /llms/manual/v13/:splat 307
/llms/manual/v12.0.0/llms.txt /llms/manual/v12/llms.txt 307
/llms/manual/v12.0.0/llms-full.txt /llms/manual/v12/llm-full.txt 307
/llms/manual/v12.0.0/llms-small.txt /llms/manual/v12/llm-small.txt 307
/llms/manual/v12.0.0/* /llms/manual/v12/:splat 307
/llms/manual/v11/llms.txt /llms/manual/v12/llms.txt 307
/llms/manual/v11/llms-full.txt /llms/manual/v12/llm-full.txt 307
/llms/manual/v11/llms-small.txt /llms/manual/v12/llm-small.txt 307
/llms/manual/v11/* /llms/manual/v12/:splat 307
/llms/manual/v11.0.0/llms.txt /llms/manual/v12/llms.txt 307
/llms/manual/v11.0.0/llms-full.txt /llms/manual/v12/llm-full.txt 307
/llms/manual/v11.0.0/llms-small.txt /llms/manual/v12/llm-small.txt 307
/llms/manual/v11.0.0/* /llms/manual/v12/:splat 307
/llms/manual/next/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/next/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v13.0.0/llms.txt /llms.txt 307
/llms/manual/v13.0.0/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v13.0.0/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v13/llms.txt /llms.txt 307
/llms/manual/v13/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v13/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v12.0.0/llms.txt /llms.txt 307
/llms/manual/v12.0.0/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v12.0.0/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v12/llms.txt /llms.txt 307
/llms/manual/v12/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v12/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v11/llms.txt /llms.txt 307
/llms/manual/v11/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v11/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v11.0.0/llms.txt /llms.txt 307
/llms/manual/v11.0.0/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v11.0.0/llms-small.txt /llms/manual/llm-small.txt 307
/llms/manual/v10/llms.txt /llms.txt 307
/llms/manual/v10/llms-full.txt /llms/manual/llm-full.txt 307
/llms/manual/v10/llms-small.txt /llms/manual/llm-small.txt 307

# Explicit aliases precede wildcard rules.
/docs/manual/v12.0.0/* /docs/manual/:splat 308
/docs/manual/next/* /docs/manual/:splat 308
/docs/react/latest/* /docs/react/:splat 308
/llms/manual/latest/* /llms/manual/:splat 307
/llms/manual/next/* /llms/manual/:splat 307
/llms/manual/v13.0.0/* /llms/manual/:splat 307
/llms/manual/v13/* /llms/manual/:splat 307
/llms/manual/v12.0.0/* /llms/manual/:splat 307
/llms/manual/v12/* /llms/manual/:splat 307
/llms/manual/v11/* /llms/manual/:splat 307
/llms/manual/v11.0.0/* /llms/manual/:splat 307
/llms/manual/v10/* /llms/manual/:splat 307

/docs/manual/v11.0.0/* https://v11.rescript-lang.org/docs/manual/v11.0.0/:splat 308
/docs/manual/v10.0.0/* https://v11.rescript-lang.org/docs/manual/v10.0.0/:splat 308
Expand Down
40 changes: 40 additions & 0 deletions apps/docs/scripts/__tests__/generate-llms.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { checkRedirects } from "../test-redirects.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -19,6 +20,11 @@ let writeFile = (root, filePath, content) => {

let makeWorkspace = () => {
let root = fs.mkdtempSync(path.join(os.tmpdir(), "generate llms-"));
writeFile(
root,
"public/_redirects",
fs.readFileSync(path.join(docsRoot, "public/_redirects"), "utf8"),
);

writeFile(
root,
Expand Down Expand Up @@ -206,13 +212,20 @@ order: 4
let readFile = (root, filePath) =>
fs.readFileSync(path.join(root, filePath), "utf8");

let checkWorkspaceRedirects = (root) =>
checkRedirects({
redirectsPath: path.join(root, "public", "_redirects"),
publicDirectory: path.join(root, "public"),
});

test("generate_llms writes the default manual index at the site root", () => {
let root = makeWorkspace();

child_process.execFileSync(process.execPath, [generatorPath], {
cwd: root,
stdio: "pipe",
});
checkWorkspaceRedirects(root);

let currentLlms = readFile(root, "public/llms.txt");
let humanLlmsPage = readFile(root, "markdown-pages/docs/manual/llms.mdx");
Expand Down Expand Up @@ -344,6 +357,33 @@ test("generate_llms writes the default manual index at the site root", () => {
assert.doesNotMatch(currentLlms, /v10\.|v11\.|v12\.|v13\./);
});

test("redirect checks reject a target removed after LLM generation", (t) => {
const root = makeWorkspace();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
child_process.execFileSync(process.execPath, [generatorPath], { cwd: root });
fs.unlinkSync(path.join(root, "public/llms/manual/llm-full.txt"));
assert.throws(
() => checkWorkspaceRedirects(root),
/Missing generated LLM text file.*\/llms\/manual\/llm-full\.txt/,
);
});

test("redirect checks reject a wildcard that shadows the manual index", (t) => {
const root = makeWorkspace();
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
child_process.execFileSync(process.execPath, [generatorPath], { cwd: root });
const redirects = readFile(root, "public/_redirects");
writeFile(
root,
"public/_redirects",
`/llms/manual/v12.0.0/* /llms/manual/:splat 307\n${redirects}`,
);
assert.throws(
() => checkWorkspaceRedirects(root),
/First matching redirect for \/llms\/manual\/v12\.0\.0\/llms\.txt/,
);
});

test("generate_llms writes ReScript React files", () => {
let root = makeWorkspace();

Expand Down
178 changes: 93 additions & 85 deletions apps/docs/scripts/test-redirects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,105 @@ import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const redirectsPath = path.resolve(__dirname, "../public/_redirects");

const entries = fs
.readFileSync(redirectsPath, "utf8")
.split(/\r?\n/)
.map((line, index) => ({ index, line: line.trim() }))
.filter(({ line }) => line !== "" && !line.startsWith("#"))
.map(({ index, line }) => {
const [source, destination, status] = line.split(/\s+/);
return { index, source, destination, status };
});
export function checkRedirects({
redirectsPath = path.resolve(__dirname, "../public/_redirects"),
publicDirectory = path.resolve(__dirname, "../public"),
} = {}) {
const entries = fs
.readFileSync(redirectsPath, "utf8")
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line !== "" && !line.startsWith("#"))
.map((line) => {
const [source, destination, status] = line.split(/\s+/);
return { source, destination, status };
});

let findEntry = (source) => {
const entry = entries.find((entry) => entry.source === source);
assert.ok(entry, `Missing redirect for ${source}`);
return entry;
};
const assertRedirect = (source, destination, status) => {
const entry = entries.find((entry) => entry.source === source);
assert.ok(entry, `Missing redirect for ${source}`);
assert.equal(entry.destination, destination, source);
assert.equal(entry.status, status, source);
return entry;
};

let assertRedirect = (source, destination, status) => {
const entry = findEntry(source);
assert.equal(entry.destination, destination);
assert.equal(entry.status, status);
return entry;
};
const assertTextTarget = (source, destination) => {
// Cloudflare uses the first matching rule, including wildcard rules.
const entry = entries.find(({ source: pattern }) =>
pattern.endsWith("*")
? source.startsWith(pattern.slice(0, -1))
: pattern === source,
);
assert.ok(entry, `Missing redirect for ${source}`);
const splat = source.slice(entry.source.length - 1);
assert.equal(
entry.destination.replace(":splat", splat),
destination,
`First matching redirect for ${source}`,
);
assert.equal(entry.status, "307", source);

let assertManualLlmsVersionRedirects = (sourceVersion, destinationVersion) => {
const sourcePrefix = `/llms/manual/${sourceVersion}`;
const destinationPrefix = `/llms/manual/${destinationVersion}`;
const index = assertRedirect(
`${sourcePrefix}/llms.txt`,
`${destinationPrefix}/llms.txt`,
"307",
);
const full = assertRedirect(
`${sourcePrefix}/llms-full.txt`,
`${destinationPrefix}/llm-full.txt`,
"307",
);
const small = assertRedirect(
`${sourcePrefix}/llms-small.txt`,
`${destinationPrefix}/llm-small.txt`,
"307",
);
const wildcard = assertRedirect(
`${sourcePrefix}/*`,
`${destinationPrefix}/:splat`,
"307",
);
const target = path.join(publicDirectory, destination.slice(1));
assert.ok(
fs.existsSync(target) && fs.statSync(target).isFile(),
`Missing generated LLM text file for ${source}: ${destination}`,
);
assert.ok(
fs.readFileSync(target, "utf8").trim().length > 0,
`Empty generated LLM text file: ${destination}`,
);
};

assert.ok(
index.index < wildcard.index,
`${sourcePrefix}/llms.txt must be listed before ${sourcePrefix}/*`,
);
assert.ok(
full.index < wildcard.index,
`${sourcePrefix}/llms-full.txt must be listed before ${sourcePrefix}/*`,
);
assert.ok(
small.index < wildcard.index,
`${sourcePrefix}/llms-small.txt must be listed before ${sourcePrefix}/*`,
assertRedirect(
"/docs/guidelines/publishing-packages",
"/docs/guides/publishing-packages",
"308",
);
};

assertRedirect("/llms/manual/llms.txt", "/llms.txt", "307");
assertRedirect(
"/docs/guidelines/publishing-packages",
"/docs/guides/publishing-packages",
"308",
);
const latestAlias = assertRedirect(
"/llms/manual/latest/llms.txt",
"/llms.txt",
"307",
);
const nextAlias = assertRedirect(
"/llms/manual/next/llms.txt",
"/llms.txt",
"307",
);
assertManualLlmsVersionRedirects("v13.0.0", "v13");
assertManualLlmsVersionRedirects("v12.0.0", "v12");
assertManualLlmsVersionRedirects("v11", "v12");
assertManualLlmsVersionRedirects("v11.0.0", "v12");
const textFiles = [
["llms.txt", "/llms.txt"],
["llms-full.txt", "/llms/manual/llm-full.txt"],
["llms-small.txt", "/llms/manual/llm-small.txt"],
];
for (const [file, destination] of textFiles) {
const source = `/llms/manual/${file}`;
assertRedirect(source, destination, "307");
assertTextTarget(source, destination);
}

assert.ok(
latestAlias.index < findEntry("/llms/manual/latest/*").index,
"/llms/manual/latest/llms.txt must be listed before /llms/manual/latest/*",
);
assert.ok(
nextAlias.index < findEntry("/llms/manual/next/*").index,
"/llms/manual/next/llms.txt must be listed before /llms/manual/next/*",
);
for (const alias of [
"latest",
"next",
"v13.0.0",
"v13",
"v12.0.0",
"v12",
"v11.0.0",
"v11",
"v10",
]) {
const prefix = `/llms/manual/${alias}`;
for (const [file, destination] of textFiles) {
const source = `${prefix}/${file}`;
assertRedirect(source, destination, "307");
assertTextTarget(source, destination);
}
assertRedirect(`${prefix}/*`, "/llms/manual/:splat", "307");
for (const file of [
"llm-full.txt",
"llm-small.txt",
"language-overview/llm.txt",
"javascript-interop/llm.txt",
"build-system/llm.txt",
"getting-started/llm.txt",
]) {
assertTextTarget(`${prefix}/${file}`, `/llms/manual/${file}`);
}
}
}

console.log("✅ Redirect check complete. 0 issues found.");
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
checkRedirects();
console.log("✅ Redirect check complete. 0 issues found.");
}
Loading