[Docs Site] Fixes all Astro TypeScript issues (#16457)

* fix: many many typescript issues

chore: bump dependencies

* fix: assign-pr script when no codeowners found

* fix: ExternalResources TS

* fix: check all functions

* chore: minor dep bumps

* chore: fixups

* chore: merge fixups
This commit is contained in:
James Ross 2024-09-13 21:32:17 +01:00 committed by GitHub
parent 42347a1d2f
commit 8ccb6d75ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 4905 additions and 4227 deletions

View file

@ -35,8 +35,11 @@ jobs:
${{ runner.os }}-node-
- run: npm ci
- run: npm run check
- run: npx astro build
## TODO: formatting checks
- run: npm run build
env:
NODE_OPTIONS: "--max-old-space-size=4192"

1
.node-version Normal file
View file

@ -0,0 +1 @@
22

View file

@ -4,85 +4,85 @@ import core from "@actions/core";
const navigationTimeout = 120000; // Set the navigation timeout to 120 seconds (120,000 milliseconds)
function arrayToHTMLList(array) {
let html = "<ul>";
let html = "<ul>";
for (let i = 0; i < array.length; i++) {
html += "<li>" + array[i] + "</li>";
}
for (let i = 0; i < array.length; i++) {
html += "<li>" + array[i] + "</li>";
}
html += "</ul>";
html += "</ul>";
return html;
return html;
}
async function checkLinks() {
const browser = await puppeteer.launch({
headless: "new",
});
const page = await browser.newPage();
const browser = await puppeteer.launch({
headless: "new",
});
const page = await browser.newPage();
const sitemapUrl = "https://developers.cloudflare.com/sitemap.xml";
await page.goto(sitemapUrl, { timeout: navigationTimeout });
const sitemapUrl = "https://developers.cloudflare.com/sitemap.xml";
await page.goto(sitemapUrl, { timeout: navigationTimeout });
const sitemapLinks = await page.$$eval("url loc", (elements) =>
elements.map((el) => el.textContent)
);
const sitemapLinks = await page.$$eval("url loc", (elements) =>
elements.map((el) => el.textContent),
);
const visitedLinks = [];
const brokenLinks = [];
const visitedLinks = [];
const brokenLinks = [];
for (const link of sitemapLinks) {
if (!link) {
continue; // Skip if the link is empty
}
for (const link of sitemapLinks) {
if (!link) {
continue; // Skip if the link is empty
}
await page.goto(link, {
waitUntil: "networkidle0",
timeout: navigationTimeout,
});
await page.goto(link, {
waitUntil: "networkidle0",
timeout: navigationTimeout,
});
const pageLinks = await page.$$eval("a", (elements) =>
elements.map((el) => el.href)
);
const pageLinks = await page.$$eval("a", (elements) =>
elements.map((el) => el.href),
);
for (const pageLink of pageLinks) {
if (!pageLink || visitedLinks.includes(pageLink)) {
continue; // Skip if the pageLink is empty or has already been visited
}
for (const pageLink of pageLinks) {
if (!pageLink || visitedLinks.includes(pageLink)) {
continue; // Skip if the pageLink is empty or has already been visited
}
if (
pageLink.includes("developers.cloudflare.com/api/operations/") ||
pageLink.startsWith("/api/operations/")
) {
console.log(`Evaluating link: ${pageLink}`);
await page.goto(pageLink, {
waitUntil: "networkidle0",
timeout: navigationTimeout,
});
visitedLinks.push(pageLink);
if (
pageLink.includes("developers.cloudflare.com/api/operations/") ||
pageLink.startsWith("/api/operations/")
) {
console.log(`Evaluating link: ${pageLink}`);
await page.goto(pageLink, {
waitUntil: "networkidle0",
timeout: navigationTimeout,
});
visitedLinks.push(pageLink);
const statusCode = await page.evaluate(() => {
return {
url: window.location.href,
};
});
if (statusCode.url === "https://developers.cloudflare.com/api/") {
brokenLinks.push(pageLink);
}
}
}
}
const statusCode = await page.evaluate(() => {
return {
url: window.location.href,
};
});
if (statusCode.url === "https://developers.cloudflare.com/api/") {
brokenLinks.push(pageLink);
}
}
}
}
await browser.close();
console.log("Broken links:");
console.log(brokenLinks);
if (brokenLinks.length > 0) {
core.setOutput("brokenLinks", arrayToHTMLList(brokenLinks));
}
process.exit(0);
await browser.close();
console.log("Broken links:");
console.log(brokenLinks);
if (brokenLinks.length > 0) {
core.setOutput("brokenLinks", arrayToHTMLList(brokenLinks));
}
process.exit(0);
}
checkLinks().catch((error) => {
console.error(error);
process.exit(1);
console.error(error);
process.exit(1);
});

View file

@ -1,3 +1,4 @@
// @ts-nocheck TODO: refactor this for modern Astro, or remove
/**
* 1. Crawl the `/public` directory (HTML files) and assert:
* - all anchor tags (<a>) do not point to broken links
@ -22,229 +23,229 @@ const VERBOSE = process.argv.includes("--verbose");
const EXTERNALS = process.argv.includes("--externals");
async function walk(dir: string) {
let files = await fs.readdir(dir);
await Promise.all(
files.map(async (name) => {
let abs = join(dir, name);
if (name.endsWith(".html")) return task(abs);
let stats = await fs.stat(abs);
if (stats.isDirectory()) return walk(abs);
})
);
let files = await fs.readdir(dir);
await Promise.all(
files.map(async (name) => {
let abs = join(dir, name);
if (name.endsWith(".html")) return task(abs);
let stats = await fs.stat(abs);
if (stats.isDirectory()) return walk(abs);
}),
);
}
let CACHE = new Map<string, boolean>();
function HEAD(url: string): Promise<boolean> {
let value = CACHE.has(url);
if (value != null) return Promise.resolve(value);
let value = CACHE.has(url);
if (value != null) return Promise.resolve(value);
let options: https.RequestOptions = {
method: "HEAD",
headers: {
"user-agent": "dev-docs",
},
};
let options: https.RequestOptions = {
method: "HEAD",
headers: {
"user-agent": "dev-docs",
},
};
if (url.startsWith("http://")) {
options.agent = http.globalAgent;
}
if (url.startsWith("http://")) {
options.agent = http.globalAgent;
}
let req = https.request(url, options);
let req = https.request(url, options);
return new Promise((r) => {
req.on("error", (err) => {
console.log(url, err);
CACHE.set(url, false);
return r(false);
});
return new Promise((r) => {
req.on("error", (err) => {
console.log(url, err);
CACHE.set(url, false);
return r(false);
});
req.on("response", (res) => {
let bool = res.statusCode > 199 && res.statusCode < 400;
console.log({ url, bool });
CACHE.set(url, bool);
return r(bool);
});
req.on("response", (res) => {
let bool = res.statusCode > 199 && res.statusCode < 400;
console.log({ url, bool });
CACHE.set(url, bool);
return r(bool);
});
req.end();
});
req.end();
});
}
interface Message {
type: "error" | "warn";
html?: string;
value?: string;
text?: string;
type: "error" | "warn";
html?: string;
value?: string;
text?: string;
}
async function testREDIRECTS(file: string) {
const textPlaceholder = await fs.readFile(file, "utf-8");
const destinationURLRegex = new RegExp(/\/.*\/*? (\/.*\/)/);
const textPlaceholder = await fs.readFile(file, "utf-8");
const destinationURLRegex = new RegExp(/\/.*\/*? (\/.*\/)/);
for (const line of textPlaceholder.split(/[\r\n]+/)) {
let exists = false;
if (!line.startsWith("#")) {
const result = line.match(destinationURLRegex);
if (result !== null) {
const match = result[1];
if (match.startsWith('/api/')) {
return;
} else {
let local = join(PUBDIR, match);
exists = existsSync(local);
for (const line of textPlaceholder.split(/[\r\n]+/)) {
let exists = false;
if (!line.startsWith("#")) {
const result = line.match(destinationURLRegex);
if (result !== null) {
const match = result[1];
if (match.startsWith("/api/")) {
return;
} else {
let local = join(PUBDIR, match);
exists = existsSync(local);
if (!exists) {
REDIRECT_ERRORS.push(`\n ✘ ${result[0]}`);
}
}
}
}
}
if (!exists) {
REDIRECT_ERRORS.push(`\n ✘ ${result[0]}`);
}
}
}
}
}
}
async function task(file: string) {
let html = await fs.readFile(file, "utf8");
let html = await fs.readFile(file, "utf8");
let document = parse(html, {
comment: false,
blockTextElements: {
script: false,
noscript: false,
style: false,
pre: false,
},
});
let document = parse(html, {
comment: false,
blockTextElements: {
script: false,
noscript: false,
style: false,
pre: false,
},
});
let placeholder = "http://foo.io";
// build this file's URL; without "index.html" at end
let self = file
.substring(PUBDIR.length, file.length - 10)
.replace(/\\+/g, "/");
let url = new URL(self, placeholder);
let placeholder = "http://foo.io";
// build this file's URL; without "index.html" at end
let self = file
.substring(PUBDIR.length, file.length - 10)
.replace(/\\+/g, "/");
let url = new URL(self, placeholder);
let messages: Message[] = [];
let items = document.querySelectorAll("a[href],img[src]");
let messages: Message[] = [];
let items = document.querySelectorAll("a[href],img[src]");
await Promise.all(
items.map(async (item) => {
let content = item.outerHTML;
let target = item.getAttribute("src") || item.getAttribute("href");
await Promise.all(
items.map(async (item) => {
let content = item.outerHTML;
let target = item.getAttribute("src") || item.getAttribute("href");
if (!target && item.rawTagName === "a") {
// parsing error; this is actually `<a ... href=/>
if (/logo-link/.test(item.classNames)) return;
return messages.push({
type: "warn",
html: content,
text: `Missing "href" value`,
});
}
if (!target && item.rawTagName === "a") {
// parsing error; this is actually `<a ... href=/>
if (/logo-link/.test(item.classNames)) return;
return messages.push({
type: "warn",
html: content,
text: `Missing "href" value`,
});
}
if (target && (target.startsWith("/api/") || target === "/api")) {
return;
}
if (target && (target.startsWith("/api/") || target === "/api")) {
return;
}
if (target && target.includes("discord.gg/cloudflaredev")) {
return messages.push({
type: "error",
html: content,
text: "Use 'https://discord.cloudflare.com' instead of 'https://discord.gg/cloudflaredev'.",
});
}
if (target && target.includes("discord.gg/cloudflaredev")) {
return messages.push({
type: "error",
html: content,
text: "Use 'https://discord.cloudflare.com' instead of 'https://discord.gg/cloudflaredev'.",
});
}
let exists: boolean;
let external = false;
let resolved = new URL(target, url);
let exists: boolean;
let external = false;
let resolved = new URL(target, url);
if (!/https?/.test(resolved.protocol)) return;
if (!/https?/.test(resolved.protocol)) return;
if ((external = resolved.origin !== placeholder)) {
// only fetch external URLs with `--externals` flag
exists = EXTERNALS ? await HEAD(target) : true;
}
if ((external = resolved.origin !== placeholder)) {
// only fetch external URLs with `--externals` flag
exists = EXTERNALS ? await HEAD(target) : true;
}
if (!external) {
let local = join(PUBDIR, resolved.pathname);
if (!external) {
let local = join(PUBDIR, resolved.pathname);
// is this HTML page? eg; "/foo/"
if (extname(local).length === 0) {
// TODO? log warning about no trailing slash
if (!local.endsWith("/")) local += "/";
local += "index.html";
}
// is this HTML page? eg; "/foo/"
if (extname(local).length === 0) {
// TODO? log warning about no trailing slash
if (!local.endsWith("/")) local += "/";
local += "index.html";
}
exists = existsSync(local);
}
exists = existsSync(local);
}
if (!exists) {
messages.push({
type: "error",
html: content,
value: target,
});
}
})
);
if (!exists) {
messages.push({
type: "error",
html: content,
value: target,
});
}
}),
);
if (messages.length > 0) {
let output = file.substring(PUBDIR.length);
if (messages.length > 0) {
let output = file.substring(PUBDIR.length);
messages.forEach((msg) => {
if (msg.type === "error") {
output += "\n ✘";
ERRORS++;
} else {
output += "\n ⚠";
WARNS++;
}
output += " " + (msg.text || msg.value);
if (VERBOSE) output += "\n " + msg.html;
});
messages.forEach((msg) => {
if (msg.type === "error") {
output += "\n ✘";
ERRORS++;
} else {
output += "\n ⚠";
WARNS++;
}
output += " " + (msg.text || msg.value);
if (VERBOSE) output += "\n " + msg.html;
});
console.log(output + "\n");
}
console.log(output + "\n");
}
}
try {
await walk(PUBDIR);
await walk(PUBDIR);
if (!ERRORS && !WARNS) {
console.log("\n~> Regular files DONE~!\n\n");
} else {
let msg = "\n~> Regular files DONE with:";
if (ERRORS > 0) {
process.exitCode = 1;
msg += "\n - " + ERRORS.toLocaleString() + " error(s)";
}
if (WARNS > 0) {
msg += "\n - " + WARNS.toLocaleString() + " warning(s)";
}
console.log(msg + "\n\n");
}
if (!ERRORS && !WARNS) {
console.log("\n~> Regular files DONE~!\n\n");
} else {
let msg = "\n~> Regular files DONE with:";
if (ERRORS > 0) {
process.exitCode = 1;
msg += "\n - " + ERRORS.toLocaleString() + " error(s)";
}
if (WARNS > 0) {
msg += "\n - " + WARNS.toLocaleString() + " warning(s)";
}
console.log(msg + "\n\n");
}
} catch (err) {
console.error(err.stack || err);
process.exit(1);
console.error(err.stack || err);
process.exit(1);
}
try {
await testREDIRECTS(REDIRECT_FILE);
if (REDIRECT_ERRORS.length == 0) {
console.log("\n~> /content/_redirects file DONE~!\n\n");
} else {
let msg = "\n~> /content/_redirects file DONE with:";
process.exitCode = 1;
msg +=
"\n - " +
REDIRECT_ERRORS.length.toLocaleString() +
" error(s)" +
" (due to bad destination URLs)" +
"\n\n";
for (let i = 0; i < REDIRECT_ERRORS.length; i++) {
msg += REDIRECT_ERRORS[i];
}
console.log(msg + "\n\n");
}
await testREDIRECTS(REDIRECT_FILE);
if (REDIRECT_ERRORS.length == 0) {
console.log("\n~> /content/_redirects file DONE~!\n\n");
} else {
let msg = "\n~> /content/_redirects file DONE with:";
process.exitCode = 1;
msg +=
"\n - " +
REDIRECT_ERRORS.length.toLocaleString() +
" error(s)" +
" (due to bad destination URLs)" +
"\n\n";
for (let i = 0; i < REDIRECT_ERRORS.length; i++) {
msg += REDIRECT_ERRORS[i];
}
console.log(msg + "\n\n");
}
} catch (err) {
console.error(err.stack || err);
process.exit(1);
console.error(err.stack || err);
process.exit(1);
}

View file

@ -1,91 +0,0 @@
import * as fs from 'fs/promises';
import { join, resolve } from 'path';
import { Callback, Pool } from './pool';
import { options } from './prettier.config';
import type { Result } from './worker';
const ROOT = resolve('.');
const ROOTLEN = ROOT.length + 1;
const isSILENT = process.argv.includes('--quiet');
const isCHECK = process.argv.includes('--check');
const isFILE = /\.(mdx?|[mc]?[tj]sx?|json|ya?ml|s?css)$/;
// Unknown languages / missing parsers
const Missing = new Set<string>();
let warns = 0;
let errors = 0;
const task = new Pool({
script: join(ROOT, 'bin/worker.ts'),
spawn: {
execArgv: ['--loader', 'tsm'],
env: { NODE_NO_WARNINGS: '1' },
workerData: { isCHECK, ROOTLEN, options },
},
});
const handler: Callback<Result> = (err, result) => {
if (err) return console.error(err);
errors += result.error;
warns += result.warn;
if (result.warn && isCHECK) process.exitCode = 1;
else if (result.error) process.exitCode = 1;
result.missing.forEach(x => {
Missing.add(x);
});
};
async function walk(dir: string): Promise<void> {
let files = await fs.readdir(dir);
await Promise.all(
files.map(fname => {
let absolute = join(dir, fname);
if (fname === '.github') return walk(absolute);
if (fname === 'node_modules' || fname === 'public') return;
if (/^[._]/.test(fname) || /\.hbs$/.test(fname)) return;
// TODO: temporarily disable `*.hbs` formatting
if (isFILE.test(fname)) {
return task.run(absolute, handler);
}
return fs.stat(absolute).then(stats => {
if (stats.isDirectory()) return walk(absolute);
});
})
);
}
try {
await walk(ROOT);
if (Missing.size > 0) {
let langs = [...Missing].sort();
console.warn('\n\nMissing parser for language(s):\n');
console.warn(langs.map(x => ' - ' + x).join('\n'));
}
if (errors || warns) {
console.error('\n');
if (errors) {
console.error('Finished with %d error(s)', errors);
}
if (isCHECK && warns) {
console.error('Finished with %d warning(s)', warns);
}
console.error('\n');
isSILENT || process.exit(1);
}
} catch (err) {
console.error(err.stack || err);
process.exit(1);
}

View file

@ -1,223 +0,0 @@
import prettier from 'prettier';
import * as fs from 'fs/promises';
import { join, resolve } from 'path';
import { langs } from './prism.config';
import { options } from './prettier.config';
const ROOT = resolve('.');
const ROOTLEN = ROOT.length + 1;
const isSILENT = process.argv.includes('--quiet');
const isCHECK = process.argv.includes('--check');
const isBAIL = process.argv.includes('--bail');
const isMD = /\.md$/;
const isFILE = /\.([mc]?[tj]sx?|json|ya?ml|s?css)$/;
const YAML = /^\s*(---[^]+(?:---\r?\n))/;
// Unknown languages / missing parsers
const Missing = new Set<string>();
// Prism languages to ignore
const Ignores = new Set(['txt', 'diff', 'bash', 'sh', 'toml']);
// Prism language -> prettier parser
export const Parsers: Record<string, prettier.BuiltInParserName> = {
js: 'babel',
javascript: 'babel',
md: 'mdx',
markdown: 'mdx',
mdx: 'mdx',
json: 'json',
json5: 'json5',
ts: 'typescript',
typescript: 'typescript',
gql: 'graphql',
graphql: 'graphql',
xml: 'html',
html: 'html',
svelte: 'html',
hbs: 'html',
vue: 'vue',
yaml: 'yaml',
yml: 'yaml',
};
interface Metadata {
file: string;
lang: string;
content?: string;
}
let warns = 0;
let errors = 0;
function toError(msg: string, meta: Metadata): void {
errors++;
msg += '\n~> file: ' + meta.file.substring(ROOTLEN);
msg += '\n~> language: ' + meta.lang;
if (meta.content) {
msg += '\n~> code: ';
meta.content.split(/\r?\n/g).forEach(txt => {
msg += '\n\t' + txt;
});
}
console.error('\n\n' + msg);
}
function format(code: string, lang: string) {
let parser = Parsers[lang];
if (parser == null) {
Missing.add(lang);
return code;
}
return prettier.format(code, { ...options, parser });
}
async function write(file: string, output: string, isMatch: boolean) {
let txt = isCHECK ? 'PASS' : 'OK';
if (isCHECK && !isMatch) {
process.exitCode = 1;
txt = 'FAIL';
warns++;
}
isCHECK || (await fs.writeFile(file, output));
process.stdout.write(`[${txt}] ${file.substring(ROOTLEN)}\n`);
}
async function prettify(file: string, lang?: string) {
let extn = file.substring(file.lastIndexOf('.') + 1);
let input = await fs.readFile(file, 'utf8');
let output = format(input, lang || langs[extn] || extn);
await write(file, output, input === output);
}
async function walk(dir: string): Promise<void> {
let files = await fs.readdir(dir);
await Promise.all(
files.map(fname => {
let absolute = join(dir, fname);
if (fname === '.github') return walk(absolute);
if (fname === 'node_modules' || fname === 'public') return;
if (/^[._]/.test(fname) || /\.hbs$/.test(fname)) return;
// TODO: temporarily disable `*.hbs` formatting
if (isMD.test(fname)) return markdown(absolute);
if (isFILE.test(fname)) return prettify(absolute);
return fs.stat(absolute).then(stats => {
if (stats.isDirectory()) return walk(absolute);
});
})
);
}
async function markdown(file: string): Promise<void> {
let last = 0;
let output = '';
let match: RegExpExecArray | null;
let input = await fs.readFile(file, 'utf8');
let BACKTICKS = /^( +)?([`]{3})([A-Za-z]+?)\n([^]+?)(\2)/gm;
while ((match = BACKTICKS.exec(input))) {
let [full, lead, open, hint, inner, close] = match;
let current = match.index;
output += input.substring(last, current);
lead = lead || '';
hint = hint || 'txt';
let lang = (langs[hint] || hint).toLowerCase();
if (Ignores.has(lang) || Missing.has(lang)) {
last = current + full.length;
output += full;
continue;
}
let isYAML = YAML.exec(inner);
let frontmatter = (isYAML && isYAML[1]) || '';
if (frontmatter.length > 0) {
// TODO: parse for `format: false` value
inner = inner.substring(frontmatter.length + lead.length);
if (lead.length > 0) {
frontmatter = frontmatter.replace(new RegExp('\n' + lead, 'g'), '\n');
}
}
try {
var pretty = format(inner, lang).trimEnd();
} catch (err) {
toError('Error formatting code snippet!', { file, lang });
if (isBAIL) throw err;
return console.error(err.message || err);
}
output += lead + '```' + lang + '\n';
if (lead.length > 0) {
(frontmatter + pretty).split(/\r?\n/g).forEach(line => {
output += lead + line + '\n';
});
} else {
output += frontmatter + pretty + '\n';
}
output += lead + '```';
last = current + full.length;
}
if (last && last < input.length) {
output += input.substring(last);
} else if (last < 1) {
output = input;
}
try {
output = format(output, 'mdx');
} catch (err) {
toError('Error w/ final MDX format!', { file, lang: 'mdx' });
if (isBAIL) throw err;
return console.error(err.stack || err);
}
await write(file, output, input === output);
}
try {
await walk(ROOT);
if (Missing.size > 0) {
let langs = [...Missing].sort();
console.warn('\n\nMissing parser for language(s):\n');
console.warn(langs.map(x => ' - ' + x).join('\n'));
}
if (errors || warns) {
console.error('\n');
if (errors) {
console.error('Finished with %d error(s)', errors);
}
if (isCHECK && warns) {
console.error('Finished with %d warning(s)', warns);
}
console.error('\n');
isSILENT || process.exit(1);
}
} catch (err) {
console.error(err.stack || err);
process.exit(1);
}

View file

@ -1,111 +0,0 @@
/**
* tsm bin/highlight.ts replace
* tsm bin/highlight.ts restore
*/
import * as fs from 'fs/promises';
import { join, resolve } from 'path';
import { highlight } from './prism.config';
const ROOT = resolve('.');
const ROOTLEN = ROOT.length + 1;
const CONTENT = join(ROOT, 'content');
const REPLACE = process.argv.includes('replace');
const RESTORE = !REPLACE && process.argv.includes('restore');
const isMD = /\.md$/;
const isBACKUP = /\.md\.backup$/;
// const YAML = /^\s*(---[^]+(?:---\r?\n))/;
async function walk(dir: string): Promise<void> {
let ignores = new Set(['static', 'media']);
let files = await fs.readdir(dir, { withFileTypes: true });
await Promise.all(
files.map(dirent => {
let fname = dirent.name;
let absolute = join(dir, fname);
if (isMD.test(fname)) {
if (REPLACE) return markdown(absolute);
else return;
}
if (isBACKUP.test(fname)) {
if (RESTORE) return restore(absolute);
else return;
}
if (dirent.isDirectory() && !ignores.has(fname)) {
return walk(absolute);
}
})
);
}
// mv foo.md.backup foo.md
async function restore(backup: string) {
let original = backup.substring(0, backup.length - 7);
await fs.copyFile(backup, original);
await fs.unlink(backup);
}
// @modified `bin/format.ts` function
async function markdown(file: string): Promise<void> {
let last = 0;
let output = '';
let match: RegExpExecArray | null;
let input = await fs.readFile(file, 'utf8');
let BACKTICKS = /^(\s+)?([`]{3})([A-Za-z]+)?\r?\n([^]+?)(\2)/gm;
while ((match = BACKTICKS.exec(input))) {
let current = match.index;
let [full, ws, open, hint, inner, close] = match;
output += input.substring(last, current);
ws = ws || '';
// codeblock => HTML markup
let lang = (hint || 'txt').toLowerCase();
// dedent codeblock, only if indented
let [spaces] = ws.match(/[ ]+$/) || '';
if (spaces && spaces.length > 0) {
let rgx = new RegExp('^([ ]){' + spaces.length + '}', 'gm');
inner = inner.replace(rgx, '');
}
let html = highlight(inner, lang, file.substring(ROOTLEN));
// prevent hugo from looking at "{{<" pattern
output += "\n\n" + ' '.repeat(spaces?.length ?? 0) + '{{<raw>}}' + html.replace(/\{\{\</g, '{\\{<') + '{{</raw>}}';
last = current + full.length;
}
if (last && last < input.length) {
output += input.substring(last);
} else if (last < 1) {
output = input;
}
let label = 'SKIP';
if (last > 0) {
label = 'PASS';
//~> cp foo.md foo.md.backup
await fs.writeFile(file + '.backup', input);
// overwrite with HTML replacements
await fs.writeFile(file, output);
}
process.stdout.write(`[${label}] ` + file.substring(ROOTLEN) + '\n');
}
try {
await walk(CONTENT);
console.log('~> DONE~!');
} catch (err) {
console.error(err.stack || err);
process.exit(1);
}

View file

@ -1,33 +1,33 @@
import lzstring from "lz-string";
export function serialiseWorker(code: string): FormData {
const formData = new FormData();
const formData = new FormData();
const metadata = {
main_module: "index.js",
};
const metadata = {
main_module: "index.js",
};
formData.set(
"index.js",
new Blob([code], {
type: "application/javascript+module",
}),
"index.js"
);
formData.set(
"index.js",
new Blob([code], {
type: "application/javascript+module",
}),
"index.js",
);
formData.set(
"metadata",
new Blob([JSON.stringify(metadata)], { type: "application/json" })
);
formData.set(
"metadata",
new Blob([JSON.stringify(metadata)], { type: "application/json" }),
);
return formData;
return formData;
}
export async function compressWorker(worker: FormData) {
const serialisedWorker = new Response(worker);
return lzstring.compressToEncodedURIComponent(
`${serialisedWorker.headers.get(
"content-type"
)}:${await serialisedWorker.text()}`
);
const serialisedWorker = new Response(worker);
return lzstring.compressToEncodedURIComponent(
`${serialisedWorker.headers.get(
"content-type",
)}:${await serialisedWorker.text()}`,
);
}

View file

@ -1,139 +0,0 @@
import { AsyncResource } from 'async_hooks';
import { isMainThread, parentPort, Worker } from 'worker_threads';
import { EventEmitter } from 'events';
import { cpus } from 'os';
import type { WorkerOptions } from 'worker_threads';
const FREE = Symbol('FREE');
export interface Options {
script: URL | string;
spawn?: WorkerOptions;
max?: number;
}
export type Callback<T = unknown> = (err: Error | null, result: T) => Promise<void> | void;
export interface Task<I = any, R = unknown> {
input: I;
handle?: Callback<R>;
resolve(value: R): void;
reject(reason?: any): void;
}
async function exec<R = unknown>(callback: Callback<R>, err: Error | null, result: R | null) {
let item = new AsyncResource('Task');
let output = await item.runInAsyncScope(callback, null, err, result);
item.emitDestroy(); // single use
return output;
}
export class Pool extends EventEmitter {
jobs: Map<number, Callback<unknown>>;
idles: Worker[];
workers: Set<Worker>;
script: URL | string;
tasks: Task[];
private options?: WorkerOptions;
private exit?: boolean;
constructor(options: Options) {
super();
this.idles = [];
this.script = options.script;
this.workers = new Set();
this.jobs = new Map();
this.tasks = [];
this.options = {
execArgv: [],
...options.spawn,
};
let i = 0;
let max = Math.max(1, options.max || cpus().length);
while (i++ < max) this.spawn();
this.on(FREE, () => {
if (this.tasks.length > 0) {
let task = this.tasks.shift()!;
this.dispatch(task);
}
});
}
private dispatch(task: Task): void {
if (this.idles.length < 1) {
this.tasks.push(task);
return;
}
let worker = this.idles.pop()!;
worker.once('message', async result => {
if (task.handle) {
result = await exec(task.handle, null, result);
}
worker.removeAllListeners('message');
this.idles.push(worker);
task.resolve(result);
this.emit(FREE);
});
worker.once('error', async err => {
let result;
if (task.handle) {
result = await exec(task.handle, err, null);
}
// replace current/dead worker
this.workers.delete(worker);
// TODO: options.retry
this.spawn();
if (result == null) task.reject(err);
else task.resolve(result);
});
worker.postMessage(task.input);
}
spawn(options?: WorkerOptions): void {
if (this.exit) return;
let worker = new Worker(this.script, {
...this.options,
...options,
});
this.workers.add(worker);
this.idles.push(worker);
this.emit(FREE);
}
run<T, R>(input: T, handle?: Callback<R>) {
return new Promise((resolve, reject) => {
this.dispatch({ input, handle, resolve, reject } as Task);
});
}
close(): void {
this.exit = true;
for (let worker of this.workers) {
worker.terminate();
}
}
}
export const isMain = isMainThread;
export const isWorker = !isMainThread;
export function listen<R = unknown>(callback: Callback<R>) {
if (parentPort) parentPort.on('message', callback);
else throw new Error('Missing `parentPort` link');
}

View file

@ -1,21 +0,0 @@
import type { Options } from 'prettier';
export const options: Options = {
arrowParens: 'avoid',
bracketSameLine: false,
bracketSpacing: true,
embeddedLanguageFormatting: 'auto',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxSingleQuote: false,
printWidth: 100,
proseWrap: 'preserve',
quoteProps: 'consistent',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
vueIndentScriptAndStyle: true,
};

View file

@ -1,390 +0,0 @@
import Prism from "prismjs";
import rangeParser from "parse-numeric-range";
import type { Token, TokenStream } from "prismjs";
globalThis.Prism = Prism;
import "prismjs/components/prism-bash.min.js";
import "prismjs/components/prism-c.min.js";
import "prismjs/components/prism-csharp.min.js";
import "prismjs/components/prism-csv.min.js";
import "prismjs/components/prism-diff.min.js";
import "prismjs/components/prism-git.min.js";
import "prismjs/components/prism-go.min.js";
import "prismjs/components/prism-graphql.min.js";
import "prismjs/components/prism-hcl.min.js";
import "prismjs/components/prism-http.min.js";
import "prismjs/components/prism-ini.min.js";
import "prismjs/components/prism-java.min.js";
import "prismjs/components/prism-json.min.js";
import "prismjs/components/prism-jsx.min.js";
import "prismjs/components/prism-markdown.min.js";
import "prismjs/components/prism-perl.min.js";
import "prismjs/components/prism-php.min.js";
import "prismjs/components/prism-powershell.min.js";
import "prismjs/components/prism-python.min.js";
import "prismjs/components/prism-ruby.min.js";
import "prismjs/components/prism-rust.min.js";
import "prismjs/components/prism-sql.min.js";
import "prismjs/components/prism-typescript.min.js";
import "prismjs/components/prism-toml.min.js";
import "prismjs/components/prism-yaml.min.js";
import "prismjs/components/prism-kotlin.min.js";
import "prismjs/components/prism-swift.min.js";
import { compressWorker, serialiseWorker } from "./playground";
// Custom `shell` grammar
Prism.languages.sh = {
comment: {
pattern: /(^|[^'{\\$])#.*/,
alias: "unselectable",
lookbehind: true,
},
directory: {
pattern: /^[^\r\n$*!]+(?=[$])/m,
alias: "unselectable",
},
command: {
pattern: /[$](?:[^\r\n])+/,
inside: {
prompt: {
pattern: /^[$] /,
alias: "unselectable",
},
},
},
output: {
pattern: /.(?:.*(?:[\r\n]|.$))*/,
alias: "unselectable"
}
};
const originalGrammar = Prism.languages.powershell;
// Custom `powershell` grammar
Prism.languages.powershell = {
comment: {
pattern: /(^|[^'{\\$])#.*/,
alias: "unselectable",
lookbehind: true,
},
directory: {
pattern: /^PS (?=\w:[\w\\-]+> )/m,
alias: "unselectable",
},
command: {
pattern: /\w:[\w\\-]+> [^\r\n]+/,
inside: {
'prompt': {
pattern: /^\w:[\w\\-]+> /,
alias: "unselectable",
},
'comment': originalGrammar.comment,
'string': originalGrammar.string,
'boolean': originalGrammar.boolean,
'variable': /\$\w+\b/,
'function': originalGrammar.function,
'keyword': originalGrammar.keyword,
'operator': [
{
pattern: /(^|\W)(?:!|-(?:b?(?:and|x?or)|as|(?:Not)?(?:Contains|In|Like|Match)|eq|ge|gt|is(?:Not)?|Join|le|lt|ne|not|Replace|sh[lr])\b|[*%]=?)/i,
lookbehind: true
}
],
'punctuation': originalGrammar.punctuation,
},
},
};
// Prism language aliases
export const langs: Record<string, string> = {
tf: "hcl", // terraform -> hashicorp config lang
rs: "rust",
shell: "sh",
curl: "bash",
gql: "graphql",
svelte: "html",
javascript: "js",
jsonc: "json",
typescript: "ts",
plaintext: "txt",
text: "txt",
py: "python",
vue: "html",
rb: "ruby",
};
// Custom token transforms
const transformations: Record<string, any> = {
js: {
keyword: {
to: "declaration-keyword",
for: new Set([
"const",
"let",
"var",
"async",
"await",
"function",
"class",
]),
},
punctuation: {
to: "operator",
for: new Set(["."]),
},
"class-name": {
to: "api",
for: new Set(["HTMLRewriter", "Request", "Response", "URL", "Error"]),
},
function: {
to: "builtin",
for: new Set([
"fetch",
"console",
"addEventListener",
"atob",
"btoa",
"setInterval",
"clearInterval",
"setTimeout",
"clearTimeout",
]),
},
},
};
transformations.ts = transformations.js;
transformations.html = {
keyword: transformations.js.keyword,
};
interface Node {
types: string;
content: string;
}
type Line = Node[];
const ESCAPE = /[&"<>]/g;
const CHARS = {
'"': "&quot;",
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
};
// @see lukeed/tempura
function toEscape(value: string) {
let tmp = 0;
let out = "";
let last = (ESCAPE.lastIndex = 0);
while (ESCAPE.test(value)) {
tmp = ESCAPE.lastIndex - 1;
out += value.substring(last, tmp) + CHARS[value[tmp] as keyof typeof CHARS];
last = tmp + 1;
}
return out + value.substring(last);
}
function normalize(tokens: (Token | string)[]) {
let line: Line = [];
let lines: Line[] = [];
function loop(types: string, item: TokenStream) {
if (Array.isArray(item)) {
item.forEach((x) => loop(types, x));
} else if (typeof item === "string") {
types = types || "CodeBlock--token-plain";
if (item === "") {
// ignore
} else if (item === "\n") {
line.push({ types, content: item });
lines.push(line);
line = [];
} else if (item === "\n\n") {
line.push({ types, content: "\n" });
lines.push(line);
line = [{ types: "CodeBlock--token-plain", content: "\n" }];
lines.push(line);
line = [];
} else if (item.includes("\n")) {
item.split(/\r?\n/g).forEach((txt, idx, arr) => {
if (!txt && !idx && idx < arr.length) return;
let content = txt ? toEscape(txt) : "\n";
if (idx > 0) {
lines.push(line);
line = [];
}
line.push({ types, content });
});
} else {
let content = toEscape(item);
line.push({ types, content });
}
} else if (item) {
if (types) types += " ";
types += "CodeBlock--token-" + item.type;
if (item.alias) {
([] as string[]).concat(item.alias).forEach((tt) => {
if (!types.includes(tt)) {
if (types) types += " ";
types += "CodeBlock--token-" + tt;
}
});
}
loop(types, item.content);
}
}
for (let i = 0; i < tokens.length; i++) {
loop("", tokens[i]);
}
if (line.length > 0) {
lines.push(line);
}
let arr: Line[] = [];
while ((line = lines.shift())) {
if (line.length > 1 && line[0].content === "\n") {
// remove extra leading "\n" items for non-whitespace lines
line[0].content = "";
arr.push(line);
} else {
arr.push(line);
}
}
lines = arr;
// check for useless newline
// ~> last line will be single-item Array
let last = lines.pop();
if (last.length !== 1 || last[0].content.trim().length > 1) {
lines.push(last); // add it back, was useful
}
return lines;
}
export async function highlight(
code: string,
lang: string,
file: string
): Promise<string> {
lang = langs[lang] || lang || "txt";
let grammar = Prism.languages[lang.toLowerCase()];
if (!grammar) {
console.warn('[prism] Missing "%s" grammar; using "txt" fallback', lang);
grammar = Prism.languages.txt;
}
let frontmatter: {
theme?: string | "light";
highlight?: `[${string}]` | string;
filename?: string;
header?: string;
playground?: boolean;
} = {};
// Check for a YAML frontmatter,
// and ensure it's not something like -----BEGIN CERTIFICATE-----
if (code.substring(0, 3) === "---" && code[3] != "-") {
let index = code.indexOf("---", 3);
if (index > 3) {
index += 3;
let content = code.substring(0, index);
code = code.substring(index).replace(/^(\r?\n)+/, "");
// TODO: pass in `utils.frontmatter` here
// frontmatter = utils.frontmatter(content);
let match = /^---\r?\n([\s\S]+?)\r?\n---/.exec(content);
if (match != null)
match[1].split("\n").forEach((pair) => {
let [key, ...v] = pair.split(":");
frontmatter[key.trim() as "theme"] = v.join(":").trim();
});
}
}
let highlights: Set<number>;
try {
let highlight = frontmatter.highlight;
// let range-parser do the heavy lifting. It handles all supported cases
if (highlight?.startsWith("[")) {
highlight = highlight.substring(1, highlight.length - 1);
}
const parsedRange = rangeParser(highlight || "");
highlights = new Set(parsedRange.map((x: number) => x - 1));
} catch (err) {
process.stderr.write(
`[ERROR] ${file}\nSyntax highlighting error: You must specify the lines to highlight as an array (e.g., '[2]'). Found '${frontmatter.highlight}'.\n`
);
// still throwing the original error because it could be something else
throw err;
}
// tokenize & build custom string output
let tokens = Prism.tokenize(code, grammar);
let output = "";
let theme = frontmatter.theme || "light";
output +=
'<pre class="CodeBlock CodeBlock-with-rows CodeBlock-scrolls-horizontally';
if (theme === "light") output += " CodeBlock-is-light-in-light-theme";
output += ` CodeBlock--language-${lang}" language="${lang}"`;
if (frontmatter.header) output += ` title="${frontmatter.header}">`;
else output += ">"
if (frontmatter.header)
output += `<span class="CodeBlock--header">${frontmatter.header}</span>`;
else if (frontmatter.filename)
output += `<span class="CodeBlock--filename">${frontmatter.filename}</span>`;
if (frontmatter.playground) {
const serialised = await compressWorker(serialiseWorker(code));
const playgroundUrl = `https://workers.cloudflare.com/playground#${serialised}`;
output += `<a target="__blank" href="${playgroundUrl}" class="playground-link"><svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M6.21 12.293l-3.215-4.3 3.197-4.178-.617-.842-3.603 4.712-.005.603 3.62 4.847.623-.842z"></path><path d="M7.332 1.988H6.095l4.462 6.1-4.357 5.9h1.245L11.8 8.09 7.332 1.988z"></path><path d="M9.725 1.988H8.472l4.533 6.027-4.533 5.973h1.255l4.303-5.67v-.603L9.725 1.988z"></path></svg><span> Run Worker</span></a>`;
}
output += "<code>";
output += '<span class="CodeBlock--rows">';
output += '<span class="CodeBlock--rows-content">';
let i = 0;
let row = "";
let line: Line;
let lines = normalize(tokens);
for (; i < lines.length; i++) {
line = lines[i];
row = '<span class="CodeBlock--row';
row += highlights.has(i) ? ' CodeBlock--row-is-highlighted">' : '">';
row += '<span class="CodeBlock--row-indicator"></span>';
row += '<div class="CodeBlock--row-content">';
for (let j = 0; j < line.length; j++) {
row +=
'<span class="' + line[j].types + '">' + line[j].content + "</span>";
}
output += row + "</div></span>";
}
return output + "</span></span></code></pre>";
}

View file

@ -1,56 +1,56 @@
import { readFile } from 'fs/promises';
import { readFile } from "fs/promises";
async function main( ){
const redirects = await readFile('public/_redirects', { encoding: 'utf-8' });
async function main() {
const redirects = await readFile("public/_redirects", { encoding: "utf-8" });
let numInfiniteRedirects = 0;
let numUrlsWithFragment = 0;
let numDuplicateRedirects = 0;
let redirectSourceUrls: string[] = [];
let numInfiniteRedirects = 0;
let numUrlsWithFragment = 0;
let numDuplicateRedirects = 0;
let redirectSourceUrls: string[] = [];
for (const line of redirects.split('\n')) {
if (line.startsWith('#') || line.trim() === '') continue;
for (const line of redirects.split("\n")) {
if (line.startsWith("#") || line.trim() === "") continue;
const [from, to] = line.split(' ');
const [from, to] = line.split(" ");
if (from === to) {
console.log(`✘ Found infinite redirect:\n ${from} -> ${to}`);
numInfiniteRedirects++;
}
if (from === to) {
console.log(`✘ Found infinite redirect:\n ${from} -> ${to}`);
numInfiniteRedirects++;
}
if (from.includes('#')) {
console.log(`✘ Found source URL with fragment:\n ${from}`);
numUrlsWithFragment++;
}
if (from.includes("#")) {
console.log(`✘ Found source URL with fragment:\n ${from}`);
numUrlsWithFragment++;
}
if (redirectSourceUrls.includes(from)) {
console.log(`✘ Found repeated source URL:\n ${from}`)
numDuplicateRedirects++;
} else {
redirectSourceUrls.push(from);
}
}
if (redirectSourceUrls.includes(from)) {
console.log(`✘ Found repeated source URL:\n ${from}`);
numDuplicateRedirects++;
} else {
redirectSourceUrls.push(from);
}
}
if (numInfiniteRedirects || numUrlsWithFragment || numDuplicateRedirects) {
console.log("\nDetected errors:");
if (numInfiniteRedirects || numUrlsWithFragment || numDuplicateRedirects) {
console.log("\nDetected errors:");
if (numInfiniteRedirects > 0) {
console.log(`- ${numInfiniteRedirects} infinite redirect(s)`);
}
if (numInfiniteRedirects > 0) {
console.log(`- ${numInfiniteRedirects} infinite redirect(s)`);
}
if (numUrlsWithFragment > 0) {
console.log(`- ${numUrlsWithFragment} source URL(s) with a fragment`);
}
if (numUrlsWithFragment > 0) {
console.log(`- ${numUrlsWithFragment} source URL(s) with a fragment`);
}
if (numDuplicateRedirects > 0) {
console.log(`- ${numDuplicateRedirects} repeated source URL(s)`);
}
if (numDuplicateRedirects > 0) {
console.log(`- ${numDuplicateRedirects} repeated source URL(s)`);
}
console.log("\nPlease fix the errors above before merging :)");
process.exit(1);
} else {
console.log("\nDone!");
}
console.log("\nPlease fix the errors above before merging :)");
process.exit(1);
} else {
console.log("\nDone!");
}
}
main();

View file

@ -1,184 +0,0 @@
import prettier from 'prettier';
import * as fs from 'fs/promises';
import * as thread from 'worker_threads';
import { langs } from './prism.config';
export interface Result {
file: string;
missing: string[];
error: 1 | 0;
warn: 1 | 0;
}
// Environment / inherited information
const { isCHECK, ROOTLEN, options } = thread.workerData;
const Parent = thread.parentPort!;
const YAML = /^\s*(---[^]+(?:---\r?\n))/;
// Unknown languages / missing parsers
const Missing = new Set<string>();
// Prism languages to ignore
const Ignores = new Set(['txt', 'diff', 'bash', 'sh', 'toml']);
// Prism language -> prettier parser
const Parsers: Record<string, prettier.BuiltInParserName> = {
js: 'babel',
javascript: 'babel',
md: 'mdx',
markdown: 'mdx',
mdx: 'mdx',
json: 'json',
json5: 'json5',
ts: 'typescript',
typescript: 'typescript',
gql: 'graphql',
graphql: 'graphql',
xml: 'html',
html: 'html',
svelte: 'html',
hbs: 'html',
vue: 'vue',
yaml: 'yaml',
yml: 'yaml',
};
function reply(
label: 'ERRO' | 'OK' | 'PASS' | 'FAIL',
data: Omit<Result, 'missing'>,
extra?: string
) {
let text = `[${label}] ${data.file.substring(ROOTLEN)}\n`;
if (extra) text += '\n\t' + extra.replace(/(\n)/g, '$1\t') + '\n\n';
process.stdout.write(text);
let missing = [...Missing];
let result: Result = { ...data, missing };
Parent.postMessage(result);
}
function toError(msg: string, file: string, lang: string, extra?: Error | string): never {
let error = msg;
if (lang) error += ' (lang = ' + lang + ')';
if (extra) error += '\n' + extra;
reply('ERRO', { file, error: 1, warn: 0 }, error);
throw 1;
}
function format(code: string, lang: string) {
let parser = Parsers[lang];
if (parser == null) {
Missing.add(lang);
return code;
}
return prettier.format(code, { ...options, parser });
}
async function markdown(file: string, input: string): Promise<string> {
let last = 0;
let output = '';
let match: RegExpExecArray | null;
let BACKTICKS = /^( +)?([`]{3})([A-Za-z]+?)\n([^]+?)(\2)/gm;
while ((match = BACKTICKS.exec(input))) {
let [full, lead, open, hint, inner, close] = match;
let current = match.index;
output += input.substring(last, current);
lead = lead || '';
hint = hint || 'txt';
let lang = (langs[hint] || hint).toLowerCase();
if (Ignores.has(lang) || Missing.has(lang)) {
last = current + full.length;
output += full;
continue;
}
let isYAML = YAML.exec(inner);
let frontmatter = (isYAML && isYAML[1]) || '';
if (frontmatter.length > 0) {
// TODO: parse for `format: false` value
inner = inner.substring(frontmatter.length + lead.length);
if (lead.length > 0) {
frontmatter = frontmatter.replace(new RegExp('\n' + lead, 'g'), '\n');
}
}
try {
var pretty = format(inner, lang).trimEnd();
} catch (err) {
throw toError('Error formatting code snippet!', file, lang, err.message || err);
}
output += lead + '```' + lang + '\n';
if (lead.length > 0) {
(frontmatter + pretty).split(/\r?\n/g).forEach(line => {
output += lead + line + '\n';
});
} else {
output += frontmatter + pretty + '\n';
}
output += lead + '```';
last = current + full.length;
}
if (last && last < input.length) {
output += input.substring(last);
} else if (last < 1) {
output = input;
}
try {
output = format(output, 'mdx');
} catch (err) {
throw toError('Error w/ final MDX format!', file, 'mdx', err.stack || err);
}
return output;
}
// Respond to `format.ts` task pool
Parent.on('message', async file => {
let input = await fs.readFile(file, 'utf8');
let output: string | void;
try {
if (/\.mdx?$/.test(file)) {
output = await markdown(file, input);
} else {
let extn = file.substring(file.lastIndexOf('.') + 1);
output = format(input, langs[extn] || extn);
}
if (!isCHECK && output) {
await fs.writeFile(file, output);
}
const isMatch = input === output;
const warn = +(isCHECK && !isMatch) as 1 | 0;
const label = isCHECK ? (isMatch ? 'PASS' : 'FAIL') : 'OK';
return reply(label, { file, warn, error: 0 });
} catch (err) {
if (err === 1) return; // already handled
return reply('ERRO', { file, warn: 0, error: 1 });
}
});

View file

@ -1,44 +1,47 @@
import redirects from "./redirects"
import redirects from "./redirects";
const apiBase = "https://cloudflare-api-docs-frontend.pages.dev"
const apiBase = "https://cloudflare-api-docs-frontend.pages.dev";
const rewriteStaticAssets = {
element: (element: Element) => {
const prefixAttribute = (attr: string) => {
const value = element.getAttribute(attr)
element: (element: Element) => {
const prefixAttribute = (attr: string) => {
const value = element.getAttribute(attr);
if (value.startsWith("http")) {
return
}
if (value.startsWith("http")) {
return;
}
const updatedValue = `/api/${value.startsWith('/') ? value.slice(1) : value}`
element.setAttribute(attr, updatedValue)
}
const updatedValue = `/api/${value.startsWith("/") ? value.slice(1) : value}`;
element.setAttribute(attr, updatedValue);
};
const attrs = ['href', 'src']
attrs.forEach(attr => {
if (element.getAttribute(attr)) prefixAttribute(attr)
})
}
}
const attrs = ["href", "src"];
attrs.forEach((attr) => {
if (element.getAttribute(attr)) prefixAttribute(attr);
});
},
};
export const onRequestGet: PagesFunction<{}> = async ({ request }) => {
const apiPath = "/api"
const apiPath = "/api";
const url = new URL(request.url)
const url = new URL(request.url);
let subpath = url.pathname.replace(apiPath, "")
let normalizedSubpath = subpath.slice(-1) === "/" ? subpath.substring(0, subpath.length - 1) : subpath;
if(normalizedSubpath in redirects) {
url.pathname = redirects[normalizedSubpath]
return Response.redirect(url.toString(), 301)
}
const proxyUrl = `${apiBase}/${subpath}`
const proxyResponse = await fetch(proxyUrl)
let subpath = url.pathname.replace(apiPath, "");
let normalizedSubpath =
subpath.slice(-1) === "/"
? subpath.substring(0, subpath.length - 1)
: subpath;
if (normalizedSubpath in redirects) {
url.pathname = redirects[normalizedSubpath];
return Response.redirect(url.toString(), 301);
}
const proxyUrl = `${apiBase}/${subpath}`;
const proxyResponse = await fetch(proxyUrl);
return new HTMLRewriter()
.on("script", rewriteStaticAssets)
.on("link", rewriteStaticAssets)
.on("img", rewriteStaticAssets)
.transform(proxyResponse)
}
return new HTMLRewriter()
.on("script", rewriteStaticAssets)
.on("link", rewriteStaticAssets)
.on("img", rewriteStaticAssets)
.transform(proxyResponse);
};

View file

@ -1,22 +1,22 @@
export default {
"/v4docs": "/fundamentals/api/how-to/make-api-calls/",
"/tokens": "/fundamentals/api/get-started/",
"/tokens/create": "/fundamentals/api/get-started/create-token/",
"/tokens/create/template": "/fundamentals/api/reference/template/",
"/tokens/create/permissions": "/fundamentals/api/reference/permissions/",
"/tokens/advanced": "/fundamentals/api/how-to/",
"/tokens/advanced/restrictions": "/fundamentals/api/how-to/restrict-tokens/",
"/tokens/advanced/api": "/fundamentals/api/how-to/create-via-api/",
"/keys": "/fundamentals/api/get-started/keys/",
"/limits": "/fundamentals/api/reference/limits/",
"/how-to/make-api-calls": "/fundamentals/api/how-to/make-api-calls/",
"/get-started": "/fundamentals/api/get-started/",
"/get-started/create-token": "/fundamentals/api/get-started/create-token/",
"/reference/template": "/fundamentals/api/reference/template/",
"/reference/permissions": "/fundamentals/api/reference/permissions/",
"/how-to": "/fundamentals/api/how-to/",
"/how-to/restrict-tokens": "/fundamentals/api/how-to/restrict-tokens/",
"/how-to/create-via-api": "/fundamentals/api/how-to/create-via-api/",
"/get-started/keys": "/fundamentals/api/get-started/keys/",
"/reference/limits": "/fundamentals/api/reference/limits"
}
"/v4docs": "/fundamentals/api/how-to/make-api-calls/",
"/tokens": "/fundamentals/api/get-started/",
"/tokens/create": "/fundamentals/api/get-started/create-token/",
"/tokens/create/template": "/fundamentals/api/reference/template/",
"/tokens/create/permissions": "/fundamentals/api/reference/permissions/",
"/tokens/advanced": "/fundamentals/api/how-to/",
"/tokens/advanced/restrictions": "/fundamentals/api/how-to/restrict-tokens/",
"/tokens/advanced/api": "/fundamentals/api/how-to/create-via-api/",
"/keys": "/fundamentals/api/get-started/keys/",
"/limits": "/fundamentals/api/reference/limits/",
"/how-to/make-api-calls": "/fundamentals/api/how-to/make-api-calls/",
"/get-started": "/fundamentals/api/get-started/",
"/get-started/create-token": "/fundamentals/api/get-started/create-token/",
"/reference/template": "/fundamentals/api/reference/template/",
"/reference/permissions": "/fundamentals/api/reference/permissions/",
"/how-to": "/fundamentals/api/how-to/",
"/how-to/restrict-tokens": "/fundamentals/api/how-to/restrict-tokens/",
"/how-to/create-via-api": "/fundamentals/api/how-to/create-via-api/",
"/get-started/keys": "/fundamentals/api/get-started/keys/",
"/reference/limits": "/fundamentals/api/reference/limits",
};

View file

@ -1,83 +1,89 @@
export async function onRequestGet(context) {
const cachedSchema = await context.env.API_DOCS_KV.get("schema", "json")
if (cachedSchema) {
return new Response(JSON.stringify(cachedSchema), {
headers: { 'Content-type': 'application/json' }
})
}
const cachedSchema = await context.env.API_DOCS_KV.get("schema", "json");
if (cachedSchema) {
return new Response(JSON.stringify(cachedSchema), {
headers: { "Content-type": "application/json" },
});
}
const schemaUrl = "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json"
const schemaUrl =
"https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json";
const req = new Request(schemaUrl)
const req = new Request(schemaUrl);
const cache = caches.default
let response = await cache.match(req)
const cache = caches.default;
let response = await cache.match(req);
try {
if (!response) {
response = await fetch(req)
let schema = await response.json()
try {
if (!response) {
response = await fetch(req);
let schema = await response.json();
const pathsByTag = {}
const pathsByTag = {};
Object.keys(schema.paths).forEach(key => {
const path = schema.paths[key]
const tag = Object.values(path).find(endpoint => {
const tags = endpoint.tags
return tags && tags.length && tags[0]
})
if (tag) {
if (!pathsByTag[tag]) pathsByTag[tag] = []
pathsByTag[tag].push({ path, key })
}
})
Object.keys(schema.paths).forEach((key) => {
const path = schema.paths[key];
const tag = Object.values(path).find((endpoint) => {
const tags = endpoint.tags;
return tags && tags.length && tags[0];
});
if (tag) {
if (!pathsByTag[tag]) pathsByTag[tag] = [];
pathsByTag[tag].push({ path, key });
}
});
let sortedPaths = {}
const sortedTags = Object.keys(pathsByTag).sort()
sortedTags.forEach(tag => {
const tagArray = pathsByTag[tag]
tagArray.forEach(({ key, path }) => {
if (sortedPaths[key]) console.log('key already exists')
sortedPaths[key] = path
})
})
let sortedPaths = {};
const sortedTags = Object.keys(pathsByTag).sort();
sortedTags.forEach((tag) => {
const tagArray = pathsByTag[tag];
tagArray.forEach(({ key, path }) => {
if (sortedPaths[key]) console.log("key already exists");
sortedPaths[key] = path;
});
});
// sort sortedPaths by tag
sortedPaths = Object.entries(sortedPaths).sort((a, b) => {
const aVal = a[1]
const bVal = b[1]
const firstAVal = Object.values(aVal).find(endpoint => {
const tags = endpoint.tags
return tags && tags.length && tags[0]
})
const aTag = firstAVal && firstAVal.tags[0] || ''
const firstBVal = Object.values(bVal).find(endpoint => {
const tags = endpoint.tags
return tags && tags.length && tags[0]
})
const bTag = firstBVal && firstBVal.tags[0] || ''
if (aTag < bTag) return -1
if (aTag > bTag) return 1
return 0
}).reduce((obj, [key, val]) => {
obj[key] = val
return obj
}, {})
// sort sortedPaths by tag
sortedPaths = Object.entries(sortedPaths)
.sort((a, b) => {
const aVal = a[1];
const bVal = b[1];
const firstAVal = Object.values(aVal).find((endpoint) => {
const tags = endpoint.tags;
return tags && tags.length && tags[0];
});
const aTag = (firstAVal && firstAVal.tags[0]) || "";
const firstBVal = Object.values(bVal).find((endpoint) => {
const tags = endpoint.tags;
return tags && tags.length && tags[0];
});
const bTag = (firstBVal && firstBVal.tags[0]) || "";
if (aTag < bTag) return -1;
if (aTag > bTag) return 1;
return 0;
})
.reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
let sortedSchema = Object.assign({}, schema, { paths: sortedPaths })
let sortedSchema = Object.assign({}, schema, { paths: sortedPaths });
response = new Response(JSON.stringify(sortedSchema), {
headers: { 'Content-type': 'application/json' }
})
response = new Response(JSON.stringify(sortedSchema), {
headers: { "Content-type": "application/json" },
});
const expirationTtl = 60 * 60
await context.env.API_DOCS_KV.put("schema", JSON.stringify(sortedSchema), { expirationTtl })
}
return response
} catch (err) {
console.log(err)
return fetch(req)
}
const expirationTtl = 60 * 60;
await context.env.API_DOCS_KV.put(
"schema",
JSON.stringify(sortedSchema),
{ expirationTtl },
);
}
return response;
} catch (err) {
console.log(err);
return fetch(req);
}
}

View file

@ -1,9 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"]
}
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"]
},
"include": ["./**/*.ts"]
}

5209
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,22 @@
{
"name": "cloudflare-docs-starlight",
"type": "module",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"postinstall": "patch-package",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,mjs,astro,css,json,yaml,yml}\""
"build": "astro build",
"check": "npm run check:functions && npm run check:astro",
"check:astro": "npm run sync && astro check",
"check:functions": "tsc --noEmit -p ./functions/tsconfig.json",
"dev": "astro dev",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,mjs,astro,css,json,yaml,yml}\"",
"postinstall": "patch-package && npm run sync",
"preview": "astro preview",
"start": "astro dev",
"sync": "astro sync"
},
"dependencies": {
"@astro-community/astro-embed-youtube": "^0.5.2",
"devDependencies": {
"@astro-community/astro-embed-youtube": "^0.5.3",
"@astrojs/check": "^0.9.3",
"@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6",
@ -20,43 +24,45 @@
"@astrojs/starlight-docsearch": "^0.1.1",
"@astrojs/starlight-tailwind": "^2.0.3",
"@astrojs/tailwind": "^5.1.0",
"@cloudflare/workers-types": "^4.20240903.0",
"@codingheads/sticky-header": "^1.0.2",
"@types/node": "^20.16.1",
"algoliasearch": "^4.24.0",
"astro": "^4.15.5",
"astro-breadcrumbs": "^2.3.1",
"astro-icon": "^1.1.0",
"astro-live-code": "^0.0.2",
"astro-icon": "^1.1.1",
"astro-live-code": "^0.0.3",
"date-fns": "^3.6.0",
"dot-prop": "^9.0.0",
"github-slugger": "^2.0.0",
"hastscript": "^9.0.0",
"instantsearch.css": "^8.4.0",
"instantsearch.js": "^4.73.4",
"instantsearch.css": "^8.5.0",
"instantsearch.js": "^4.74.0",
"littlefoot": "^4.1.1",
"marked": "^13.0.1",
"mathjs": "^13.0.3",
"mermaid": "^10.9.1",
"lz-string": "^1.5.0",
"marked": "^14.1.1",
"mathjs": "^13.1.1",
"mermaid": "^11.1.1",
"node-html-parser": "^6.1.13",
"patch-package": "^8.0.0",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0",
"rehype-mermaid": "^2.1.0",
"rehype-slug": "^6.0.0",
"sharp": "^0.32.5",
"sharp": "^0.33.5",
"solarflare-theme": "^0.0.2",
"starlight-image-zoom": "^0.6.0",
"starlight-package-managers": "^0.6.0",
"tailwindcss": "^3.4.4",
"starlight-image-zoom": "^0.8.0",
"starlight-package-managers": "^0.7.0",
"tailwindcss": "^3.4.10",
"tippy.js": "^6.3.7",
"typescript": "^5.5.2",
"wrangler": "^3.71.0",
"yaml": "^2.4.5"
"typescript": "^5.5.4",
"wrangler": "^3.75.0",
"yaml": "^2.5.1"
},
"devDependencies": {
"@types/node": "^20.14.12",
"lz-string": "^1.5.0",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1"
"engines": {
"node": ">=22"
},
"volta": {
"node": "22.8.0"

View file

@ -7,24 +7,24 @@ type Props = z.infer<typeof props>;
const props = z.object({
title: z.string(),
depth: z.number(),
depth: z.number().min(1).max(6),
});
const { title, depth } = props.parse(Astro.props);
const Heading = `h${depth}`;
const Heading = `h${depth}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
---
<div tabindex="-1" class=`heading-wrapper level-h${depth}`>
<Heading id={slug(title)} set:html={marked.parseInline(title)} />
<Heading id={slug(title)} set:html={marked.parseInline(title) as string} />
<a class="anchor-link" href={`#${slug(title)}`}>
<span aria-hidden class="anchor-icon">
<svg width="16" height="16" viewBox="0 0 24 24">
<path
fill="currentcolor"
d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"
/>
></path>
</svg>
</span>
</a>
</div>
</div>

View file

@ -16,24 +16,24 @@ const props = z
const { product, notificationFilter } = props.parse(Astro.props);
let notifications = (await getEntry("notifications", "index"))["data"][
let notificationsRaw = (await getEntry("notifications", "index"))["data"][
"entries"
];
if (product) {
notifications = notifications.filter(
notificationsRaw = notificationsRaw.filter(
(x) => x.associatedProducts.toLowerCase() === product.toLowerCase(),
);
}
if (notificationFilter) {
notifications = notifications.filter(
notificationsRaw = notificationsRaw.filter(
(x) => x.name.toLowerCase() === notificationFilter.toLowerCase(),
);
}
notifications = Object.groupBy(
notifications,
const notifications = Object.groupBy(
notificationsRaw,
(entry) => entry.associatedProducts,
);
@ -46,7 +46,7 @@ const showProductHeadings = !product && !notificationFilter;
.map(([product, entries]) => (
<>
{showProductHeadings && <AnchorHeading depth={2} title={product} />}
{entries.map((notification) => (
{(entries ?? []).map((notification) => (
<Details header={notification.name}>
<strong>Who is it for?</strong>
<p set:html={marked.parse(notification.audience)} />

View file

@ -17,7 +17,7 @@ const resources = await getEntry(type, "index");
const filtered = resources.data.entries.filter((entry) => {
return (
(cloudflare ? cloudflare : false) &&
(cloudflare ? entry.cloudflare : false) &&
(tags ? entry.tags?.some((v: string) => tags.includes(v)) : true) &&
(products
? entry.products?.some((v: string) => products.includes(v))
@ -31,8 +31,7 @@ filtered.sort((a, b) => Number(b.updated) - Number(a.updated));
<ul>
{
filtered.map((entry) => {
// @ts-expect-error TODO: fix resource types
const title = entry.name ?? entry.title;
const title = "name" in entry ? entry.name : entry.title;
return (
<li>
<>

View file

@ -14,7 +14,8 @@ const props = z
const { id } = props.parse(Astro.props);
const plan = await indexPlans(id);
// TODO: improve product features types
const plan = (await indexPlans(id)) as any;
// @ts-ignore types not implemented for plans JSON
const properties = plan.properties;
@ -39,8 +40,8 @@ const markdown = (content: any) => {
</thead>
<tbody>
{
Object.entries(properties).map(([k, v]) => {
const renderTitle = (title) => {
Object.entries(properties).map(([_, v]: [string, any]) => {
const renderTitle = (title: string) => {
const placeholder = "[[GLOSSARY_TOOLTIP_SNIPPETS_SUBREQUEST]]";
if (title.includes(placeholder)) {

View file

@ -27,7 +27,7 @@ if (directory) {
const examples = await getCollection("docs", (entry) => {
return (
entry.data.pcx_content_type === "example" &&
(entry.slug.startsWith(target) ||
((target && entry.slug.startsWith(target)) ||
(additionalProducts !== undefined &&
entry.data.products?.some((v: string) =>
additionalProducts?.includes(v),

View file

@ -1,6 +1,5 @@
---
import { z } from "astro:schema";
import { getCollection, getEntry } from "astro:content";
import { getCollection, getEntry, type InferEntrySchema } from "astro:content";
import { formatDistance } from "date-fns";
const currentSection = Astro.params.slug?.split("/")[0];
@ -20,13 +19,23 @@ const tutorials = await getCollection("docs", (entry) => {
entry.data.pcx_content_type === "tutorial" &&
// /src/content/r2/**/*.mdx
(entry.slug.startsWith(`${currentSection}/`) ||
// products: [R2]
entry.data.products?.some(
(v: string) => v.toUpperCase() === productTitle.toUpperCase(),
))
// products: [R2]
entry.data.products?.some(
(v: string) => v.toUpperCase() === productTitle.toUpperCase(),
))
);
});
type VideoEntry = InferEntrySchema<"videos">["entries"][number];
type FinalTutorials = {
slug?: string;
data: InferEntrySchema<"docs"> | VideoEntry;
}[];
const finalTutorials: FinalTutorials = tutorials.map((x) => ({
slug: x.slug,
data: x.data,
}));
const videos = await getEntry("videos", "index");
const filteredVideos = videos.data.entries.filter((x) =>
x.products.some(
@ -35,12 +44,12 @@ const filteredVideos = videos.data.entries.filter((x) =>
);
if (filteredVideos) {
filteredVideos.forEach((x) => tutorials.push({ data: x }));
filteredVideos.forEach((x) => finalTutorials.push({ data: x }));
}
tutorials.sort((a, b) => b.data.updated - a.data.updated);
finalTutorials.sort((a, b) => Number(b.data.updated) - Number(a.data.updated));
const timeAgo = (date: Date) => {
const timeAgo = (date?: Date) => {
if (!date) return undefined;
return formatDistance(date, new Date(), { addSuffix: true });
};
@ -57,7 +66,8 @@ const timeAgo = (date: Date) => {
</thead>
<tbody>
{
tutorials.map((tutorial) => {
finalTutorials.map((tutorial) => {
// @ts-expect-error TODO: improve types
const href = tutorial.slug ? `/${tutorial.slug}` : tutorial.data.link;
return (
<tr>

View file

@ -1,48 +1,51 @@
{
import.meta.env.PROD ? (
<>
<script
src="https://ot.www.cloudflare.com/public/vendor/onetrust/scripttemplates/otSDKStub.js"
type="text/javascript"
charset="UTF-8"
data-domain-script="b1e05d49-f072-4bae-9116-bdb78af15448"
/>
<script type="text/javascript">function OptanonWrapper() {}</script>
</>
) : (
<>
<script
src="https://ot.www.cloudflare.com/public/vendor/onetrust/scripttemplates/otSDKStub.js"
type="text/javascript"
charset="UTF-8"
data-domain-script="b1e05d49-f072-4bae-9116-bdb78af15448-test"
/>
<script type="text/javascript">function OptanonWrapper() {}</script>
</>
)
import.meta.env.PROD ? (
<>
<script
src="https://ot.www.cloudflare.com/public/vendor/onetrust/scripttemplates/otSDKStub.js"
type="text/javascript"
charset="UTF-8"
data-domain-script="b1e05d49-f072-4bae-9116-bdb78af15448"
is:inline
/>
<script type="text/javascript" is:inline>
function OptanonWrapper() {}
</script>
</>
) : (
<>
<script
src="https://ot.www.cloudflare.com/public/vendor/onetrust/scripttemplates/otSDKStub.js"
type="text/javascript"
charset="UTF-8"
data-domain-script="b1e05d49-f072-4bae-9116-bdb78af15448-test"
is:inline
/>
<script type="text/javascript" is:inline>
function OptanonWrapper() {}
</script>
</>
)
}
<span class="DocsFooter--content-additional-wrapper">
<!-- OneTrust Cookies Settings button start -->
<a
role="button"
id="ot-sdk-btn"
class="ot-sdk-show-settings"
>Cookie Settings</a
>
<!-- OneTrust Cookies Settings button end -->
<!-- OneTrust Cookies Settings button start -->
<a role="button" id="ot-sdk-btn" class="ot-sdk-show-settings"
>Cookie Settings</a
>
<!-- OneTrust Cookies Settings button end -->
</span>
<style>
#ot-sdk-btn.ot-sdk-show-settings {
border: none !important;
color: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
padding: inherit !important;
font-family: var(--sl-font-family) !important;
}
#ot-sdk-btn.ot-sdk-show-settings {
border: none !important;
color: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
padding: inherit !important;
font-family: var(--sl-font-family) !important;
}
#ot-sdk-btn.ot-sdk-show-settings:hover {
background-color: inherit !important;
}
</style>
#ot-sdk-btn.ot-sdk-show-settings:hover {
background-color: inherit !important;
}
</style>

View file

@ -1,5 +1,5 @@
---
import { getEntry } from "astro:content";
import { getEntry, type CollectionEntry } from "astro:content";
import { marked } from "marked";
import { getChangelogs } from "~/util/changelogs";
import AnchorHeading from "~/components/AnchorHeading.astro";
@ -25,13 +25,13 @@ if (page.data.changelog_file_name && page.data.changelog_file_name.length > 1) {
}
const name =
page.data.changelog_product_area_name ?? page.data.changelog_file_name[0];
page.data.changelog_product_area_name ?? page.data.changelog_file_name?.[0];
let changelogs;
if (page.data.changelog_product_area_name) {
const opts = {
filter: (entry) => {
filter: (entry: CollectionEntry<"changelogs">) => {
return entry.data.productArea === name;
},
};
@ -44,7 +44,7 @@ if (page.data.changelog_product_area_name) {
({ changelogs } = await getChangelogs(opts));
} else {
const opts = {
filter: (entry) => {
filter: (entry: CollectionEntry<"changelogs">) => {
return entry.id === name;
},
};
@ -63,7 +63,7 @@ if (!changelogs) {
changelogs.map(([date, entries]) => (
<div data-date={date}>
<AnchorHeading depth={2} title={date} />
{entries.map((entry) => (
{(entries ?? []).map((entry) => (
<div data-product={entry.product.toLowerCase()}>
{page.data.changelog_product_area_name && (
<h3 class="!mt-4">
@ -71,7 +71,7 @@ if (!changelogs) {
</h3>
)}
{entry.title && <strong>{entry.title}</strong>}
<Fragment set:html={marked.parse(entry.description)} />
<Fragment set:html={marked.parse(entry.description ?? "")} />
</div>
))}
</div>

View file

@ -25,7 +25,8 @@ const entries = Object.entries(plan);
---
{
entries.map(([key, value]) => {
// TODO: improve product features types
entries.map(([key, value]: [string, any]) => {
if (key === "title" || key === "link") return;
return (
@ -37,7 +38,7 @@ const entries = Object.entries(plan);
<a href={value.link}>{value.title}</a>
</p>
)}
{Object.entries(value.properties).map(([key, value]) => (
{Object.entries(value.properties).map(([_, value]: [string, any]) => (
<p>
<strong
set:html={marked.parseInline(
@ -57,13 +58,17 @@ const entries = Object.entries(plan);
/>
</li>
{additional_descriptions && (
<li><strong>Lite: </strong>
{value.lite ? (<Fragment
set:html={marked.parseInline(value.lite.toString())}
/>): (<Fragment
set:html={marked.parseInline(value.free.toString())}
/>)}
<li>
<strong>Lite: </strong>
{value.lite ? (
<Fragment
set:html={marked.parseInline(value.lite.toString())}
/>
) : (
<Fragment
set:html={marked.parseInline(value.free.toString())}
/>
)}
</li>
)}
<li>
@ -71,13 +76,17 @@ const entries = Object.entries(plan);
<Fragment set:html={marked.parseInline(value.pro.toString())} />
</li>
{additional_descriptions && (
<li><strong>Pro Plus: </strong>
{value.pro_plus ? (<Fragment
set:html={marked.parseInline(value.pro_plus.toString())}
/>): (<Fragment
set:html={marked.parseInline(value.pro.toString())}
/>)}
<li>
<strong>Pro Plus: </strong>
{value.pro_plus ? (
<Fragment
set:html={marked.parseInline(value.pro_plus.toString())}
/>
) : (
<Fragment
set:html={marked.parseInline(value.pro.toString())}
/>
)}
</li>
)}
<li>

View file

@ -1,7 +1,6 @@
---
import { z } from "astro:schema";
import { getEntry } from "astro:content";
import { marked } from "marked";
type Props = z.infer<typeof props>;

View file

@ -32,8 +32,8 @@ url.searchParams.set("poster", encodeURI(thumbnailUrl.toString()));
</div>
<script is:inline src="https://embed.cloudflarestream.com/embed/sdk.latest.js"
></script>
<script is:inline define:vars={{ videoId, videoTitle }}>
const video = document.getElementById(videoId);
<script is:inline define:vars={{ vidId: videoId, videoTitle }}>
const video = document.getElementById(vidId);
Stream(video).addEventListener("play", () => {
zaraz.track("play docs video", { title: videoTitle });
});

View file

@ -1,3 +0,0 @@
---
import Default from "@astrojs/starlight/components";
---

View file

@ -7,6 +7,7 @@ const troubleshootingTypes = ["troubleshooting", "faq"];
const resources = await getCollection("docs", (entry) => {
return (
entry.data.pcx_content_type &&
troubleshootingTypes.includes(entry.data.pcx_content_type) &&
entry.slug.startsWith(`${currentSection}/`)
);

View file

@ -125,23 +125,25 @@ const metrics = Object.entries({
import { evaluate, round } from "mathjs";
function calculate() {
const non_dns_udp_req_per_sec = document.querySelector(
const non_dns_udp_req_per_sec = document.querySelector<HTMLInputElement>(
"#non_dns_udp_req_per_sec",
)?.value;
const avg_non_dns_udp_session_timeout = document.querySelector(
"#avg_non_dns_udp_session_timeout",
)?.value;
const private_dns_req_per_sec = document.querySelector(
const avg_non_dns_udp_session_timeout =
document.querySelector<HTMLInputElement>(
"#avg_non_dns_udp_session_timeout",
)?.value;
const private_dns_req_per_sec = document.querySelector<HTMLInputElement>(
"#private_dns_req_per_sec",
)?.value;
const dns_udp_timeout_in_sec = document.querySelector(
const dns_udp_timeout_in_sec = document.querySelector<HTMLInputElement>(
"#dns_udp_timeout_in_sec",
)?.value;
const tcp_per_sec = document.querySelector("#tcp_per_sec")?.value;
const available_ports_per_host = document.querySelector(
const tcp_per_sec =
document.querySelector<HTMLInputElement>("#tcp_per_sec")?.value;
const available_ports_per_host = document.querySelector<HTMLInputElement>(
"#available_ports_per_host",
)?.value;
const cloudflared_replicas = document.querySelector(
const cloudflared_replicas = document.querySelector<HTMLInputElement>(
"#cloudflared_replicas",
)?.value;
@ -162,16 +164,32 @@ const metrics = Object.entries({
60`,
);
document.querySelector("#percent_capacity_per_replica").value = round(
percent_capacity_per_replica,
2,
);
document.querySelector("#percent_capacity_across_all_replicas").value =
round(percent_capacity_across_all_replicas, 2);
document.querySelector("#max_dns_request_per_min").value = round(
max_dns_request_per_min,
2,
const perceptCapacityPerReplicaInput =
document.querySelector<HTMLInputElement>("#percent_capacity_per_replica");
if (perceptCapacityPerReplicaInput) {
perceptCapacityPerReplicaInput.value = round(
percent_capacity_per_replica,
2,
);
}
const percentCapacityAcrossAllReplicasInput =
document.querySelector<HTMLInputElement>(
"#percent_capacity_across_all_replicas",
);
if (percentCapacityAcrossAllReplicasInput) {
percentCapacityAcrossAllReplicasInput.value = round(
percent_capacity_across_all_replicas,
2,
);
}
const maxDnsRequestPerMinInput = document.querySelector<HTMLInputElement>(
"#max_dns_request_per_min",
);
if (maxDnsRequestPerMinInput) {
maxDnsRequestPerMinInput.value = round(max_dns_request_per_min, 2);
}
}
document

View file

@ -1,6 +1,6 @@
---
import type { ComponentProps } from "astro/types";
import { getCollection } from "astro:content";
import { slug } from "github-slugger";
import AnchorHeading from "~/components/AnchorHeading.astro";
import InlineBadge from "~/components/InlineBadge.astro";
@ -9,14 +9,19 @@ const models = await getCollection("workers-ai-models");
const mapped = models.map((x) => x.data);
const grouped = Object.groupBy(mapped, (entry) => entry.task_type);
const entries = Object.entries(grouped).reverse();
type InlineBadgeProps = ComponentProps<typeof InlineBadge>;
---
{
entries.map(([_, models]) => (
<>
<AnchorHeading depth={2} title={models?.at(0).model.task.name} />
<AnchorHeading
depth={2}
title={models?.at(0)?.model?.task?.name ?? "Unknown"}
/>
<p>{models?.at(0).model.task.description}</p>
<p>{models?.at(0)?.model?.task?.description ?? "Unknown"}</p>
<table>
<thead>
@ -27,26 +32,27 @@ const entries = Object.entries(grouped).reverse();
</thead>
<tbody>
{models?.map((model) => {
const badges = model.model.properties.flatMap(
({ property_id, value }) => {
const badges = model.model.properties
.flatMap(({ property_id, value }): InlineBadgeProps | null => {
if (property_id === "beta" && value === "true") {
return {
preset: "beta",
};
text: "Beta",
} as const;
}
if (property_id === "lora" && value === "true") {
return {
variant: "tip",
text: "LoRA",
};
} as const;
}
if (property_id === "function_calling" && value === "true") {
return {
variant: "note",
text: "Function calling",
};
} as const;
}
if (property_id === "planned_deprecation_date") {
@ -61,9 +67,9 @@ const entries = Object.entries(grouped).reverse();
return { variant: "danger", text: "Planned deprecation" };
}
return [];
},
);
return null;
})
.filter(Boolean) as InlineBadgeProps[];
return (
<tr>

View file

@ -1,3 +1,7 @@
---
// @ts-nocheck xmlns in SVGs makes Astro very upset, so we'll just ignore this file
---
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@ -1139,4 +1143,4 @@
stroke-miterlimit="10"
pointer-events="none"></path></g
></svg
>
>

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -55,7 +55,7 @@ const blocks = [
},
],
},
];
] as const;
---
<div

View file

@ -23,6 +23,7 @@ if (currentSection) {
Astro.props.entry.data.head.push({
tag: "meta",
attrs: { property: "og:title", content: title },
content: "",
});
}
if (product.data.product.title) {
@ -58,6 +59,7 @@ if (currentSection) {
}
Astro.props.entry.data.description ??= await getPageDescription(
// @ts-expect-error TODO: improve types
Astro.props.entry,
);
@ -70,7 +72,7 @@ if (Astro.props.entry.data.pcx_content_type) {
tag: "meta",
attrs: {
name: "pcx_content_type",
content: Astro.props.entry.data.pcx_content_type,
content: contentType,
},
content: "",
});
@ -78,7 +80,7 @@ if (Astro.props.entry.data.pcx_content_type) {
tag: "meta",
attrs: {
name: "algolia_content_type",
content: Astro.props.entry.data.pcx_content_type,
content: contentType,
},
content: "",
});
@ -119,7 +121,7 @@ if (Astro.props.entry.data.updated) {
tag: "meta",
attrs: {
name: "pcx_last_reviewed",
content: daysBetween,
content: daysBetween.toString(),
},
content: "",
});
@ -128,7 +130,7 @@ if (Astro.props.entry.data.updated) {
// end metadata
if (Astro.props.entry.data.pcx_content_type === "changelog") {
const href = new URL(Astro.site);
const href = new URL(Astro.site ?? "");
href.pathname = Astro.props.entry.slug.concat("/index.xml");
Astro.props.entry.data.head.push({
@ -136,7 +138,7 @@ if (Astro.props.entry.data.pcx_content_type === "changelog") {
attrs: {
rel: "alternate",
type: "application/rss+xml",
href: href,
href: href.toString(),
},
content: "",
});

View file

@ -1,7 +1,6 @@
---
import type { Props } from "@astrojs/starlight/props";
import '@astrojs/starlight/style/markdown.css';
// @ts-expect-error no types available
import ImageZoom from "starlight-image-zoom/components/ImageZoom.astro";
/*
MIT License
@ -118,4 +117,4 @@ const { tableOfContents } = Astro.props.entry.data;
margin: 0;
}
}
</style>
</style>

View file

@ -4,31 +4,41 @@ import Default from "@astrojs/starlight/components/Sidebar.astro";
import { Icon as AstroIcon } from "astro-icon/components";
import { getEntry } from "astro:content";
import { z } from "astro:schema";
import { Badge } from "@astrojs/starlight/components";
import type { ComponentProps, HTMLAttributes } from "astro/types";
import type { AstroBuiltinAttributes } from "astro";
const { sidebar, slug } = Astro.props;
const linkHTMLAttributesSchema = z.record(
z.union([z.string(), z.number(), z.boolean(), z.undefined()]),
) as z.Schema<
Omit<HTMLAttributes<"a">, keyof AstroBuiltinAttributes | "children">
>;
type LinkHTMLAttributes = z.infer<typeof linkHTMLAttributesSchema>;
interface Link {
type: "link";
label: string;
href: string;
isCurrent: boolean;
badge: any | undefined;
attrs: any;
badge: ComponentProps<typeof Badge> | undefined;
attrs: LinkHTMLAttributes;
order: number;
}
type SidebarEntry = Link | Group;
interface Group {
type: "group";
label: string;
entries: (Link | Group)[];
entries: SidebarEntry[];
collapsed: boolean;
badge: Badge | undefined;
badge: ComponentProps<typeof Badge> | undefined;
order: number;
}
``;
type SidebarEntry = Link | Group;
const currentSection = slug?.split("/")[0];
let filtered = sidebar.filter(
@ -133,9 +143,11 @@ async function handleLink(link: Link): Promise<Link> {
...link,
label: link.label.concat(" ↗"),
href: frontmatter.external_link,
badge: frontmatter.external_link.startsWith("/api") && {
text: "API",
},
badge: frontmatter.external_link.startsWith("/api")
? {
text: "API",
}
: undefined,
};
}
@ -163,14 +175,14 @@ const lookupProductTitle = async (slug: string) => {
if (product === "learning-paths") {
const path = segments[1];
const { data } = await getEntry("learning-paths", path);
const entryData = await getEntry("learning-paths", path);
return `${data.title} (Learning Paths)`;
return `${entryData?.data?.title} (Learning Paths)`;
}
const { data } = await getEntry("products", product);
const entryData = await getEntry("products", product);
return data.product.title;
return entryData?.data?.product?.title ?? "Unknown";
};
---
@ -188,6 +200,7 @@ const lookupProductTitle = async (slug: string) => {
</strong>
</span>
</a>
<!-- @ts-expect-error sidebar props don't match as we add additional things -->
<Default {...Astro.props} sidebar={filtered.entries}><slot /></Default>
<style is:global>

View file

@ -1,7 +1,8 @@
---
/// <reference path="../../../node_modules/@astrojs/starlight/virtual.d.ts" />
import { logos } from "virtual:starlight/user-images";
import config from "virtual:starlight/user-config";
import type { Props } from "../props";
import type { Props } from "@astrojs/starlight/props";
const { siteTitle, siteTitleHref } = Astro.props;
---

View file

@ -1,5 +1,5 @@
name: Registrar
logo: <svg class="c_hf c_ck c_dw c_hg" role="presentation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8.21 1.503C8.168 1.5 8.127 1.5 8.085 1.5h-.082a6.5 6.5 0 100 13h.082A6.5 6.5 0 008.21 1.503zm4.775 4.165H11.7a7.625 7.625 0 00-.975-2.446 5.54 5.54 0 012.258 2.446zM8.5 2.561c.93.276 1.759 1.457 2.175 3.107H8.5V2.561zm-1 .061v3.046H5.496C5.886 4.12 6.64 2.983 7.5 2.622zm-1.965.464a7.47 7.47 0 00-1.066 2.582H3.022a5.538 5.538 0 012.513-2.583zm-2.556 7.15h1.469a7.555 7.555 0 001.081 2.676 5.534 5.534 0 01-2.55-2.675zM7.5 13.38c-.88-.367-1.646-1.54-2.027-3.142H7.5v3.142zm1 .06v-3.202h2.197C10.291 11.94 9.45 13.16 8.5 13.439zm2.231-.665a7.71 7.71 0 00.991-2.537h1.305a5.534 5.534 0 01-2.296 2.538zM2.643 9.237a5.522 5.522 0 01.023-2.569h10.675c.21.843.217 1.723.023 2.569H2.644z"></path><path d="M6.236 8.165h-.01l-.183-.78h-.382l-.178.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.014l.191.715h.418l.32-1.227h-.392l-.16.78zM8.177 8.165h-.011l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.013l.192.715h.418l.32-1.227h-.392l-.16.78zM10.117 8.165h-.01l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.319 1.227h.419l.191-.715h.014l.192.715h.418l.32-1.227h-.392l-.16.78zM10.881 8.222a.2.2 0 00-.147.06.193.193 0 00-.06.146.195.195 0 00.06.146.199.199 0 00.147.061c.036 0 .071-.01.102-.028a.22.22 0 00.075-.075.2.2 0 00.014-.184.195.195 0 00-.047-.066.2.2 0 00-.144-.06z"></path></svg>
logo: <svg class="c_hf c_ck c_dw c_hg" role="presentation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true"><path d="M8.21 1.503C8.168 1.5 8.127 1.5 8.085 1.5h-.082a6.5 6.5 0 100 13h.082A6.5 6.5 0 008.21 1.503zm4.775 4.165H11.7a7.625 7.625 0 00-.975-2.446 5.54 5.54 0 012.258 2.446zM8.5 2.561c.93.276 1.759 1.457 2.175 3.107H8.5V2.561zm-1 .061v3.046H5.496C5.886 4.12 6.64 2.983 7.5 2.622zm-1.965.464a7.47 7.47 0 00-1.066 2.582H3.022a5.538 5.538 0 012.513-2.583zm-2.556 7.15h1.469a7.555 7.555 0 001.081 2.676 5.534 5.534 0 01-2.55-2.675zM7.5 13.38c-.88-.367-1.646-1.54-2.027-3.142H7.5v3.142zm1 .06v-3.202h2.197C10.291 11.94 9.45 13.16 8.5 13.439zm2.231-.665a7.71 7.71 0 00.991-2.537h1.305a5.534 5.534 0 01-2.296 2.538zM2.643 9.237a5.522 5.522 0 01.023-2.569h10.675c.21.843.217 1.723.023 2.569H2.644z"></path><path d="M6.236 8.165h-.01l-.183-.78h-.382l-.178.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.014l.191.715h.418l.32-1.227h-.392l-.16.78zM8.177 8.165h-.011l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.013l.192.715h.418l.32-1.227h-.392l-.16.78zM10.117 8.165h-.01l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.319 1.227h.419l.191-.715h.014l.192.715h.418l.32-1.227h-.392l-.16.78zM10.881 8.222a.2.2 0 00-.147.06.193.193 0 00-.06.146.195.195 0 00.06.146.199.199 0 00.147.061c.036 0 .071-.01.102-.028a.22.22 0 00.075-.075.2.2 0 00.014-.184.195.195 0 00-.047-.066.2.2 0 00-.144-.06z"></path></svg>
product:
title: Registrar

View file

@ -1 +1 @@
<svg class="c_hf c_ck c_dw c_hg" role="presentation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8.21 1.503C8.168 1.5 8.127 1.5 8.085 1.5h-.082a6.5 6.5 0 100 13h.082A6.5 6.5 0 008.21 1.503zm4.775 4.165H11.7a7.625 7.625 0 00-.975-2.446 5.54 5.54 0 012.258 2.446zM8.5 2.561c.93.276 1.759 1.457 2.175 3.107H8.5V2.561zm-1 .061v3.046H5.496C5.886 4.12 6.64 2.983 7.5 2.622zm-1.965.464a7.47 7.47 0 00-1.066 2.582H3.022a5.538 5.538 0 012.513-2.583zm-2.556 7.15h1.469a7.555 7.555 0 001.081 2.676 5.534 5.534 0 01-2.55-2.675zM7.5 13.38c-.88-.367-1.646-1.54-2.027-3.142H7.5v3.142zm1 .06v-3.202h2.197C10.291 11.94 9.45 13.16 8.5 13.439zm2.231-.665a7.71 7.71 0 00.991-2.537h1.305a5.534 5.534 0 01-2.296 2.538zM2.643 9.237a5.522 5.522 0 01.023-2.569h10.675c.21.843.217 1.723.023 2.569H2.644z"></path><path d="M6.236 8.165h-.01l-.183-.78h-.382l-.178.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.014l.191.715h.418l.32-1.227h-.392l-.16.78zM8.177 8.165h-.011l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.013l.192.715h.418l.32-1.227h-.392l-.16.78zM10.117 8.165h-.01l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.319 1.227h.419l.191-.715h.014l.192.715h.418l.32-1.227h-.392l-.16.78zM10.881 8.222a.2.2 0 00-.147.06.193.193 0 00-.06.146.195.195 0 00.06.146.199.199 0 00.147.061c.036 0 .071-.01.102-.028a.22.22 0 00.075-.075.2.2 0 00.014-.184.195.195 0 00-.047-.066.2.2 0 00-.144-.06z"></path></svg>
<svg class="c_hf c_ck c_dw c_hg" role="presentation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" aria-hidden="true"><path d="M8.21 1.503C8.168 1.5 8.127 1.5 8.085 1.5h-.082a6.5 6.5 0 100 13h.082A6.5 6.5 0 008.21 1.503zm4.775 4.165H11.7a7.625 7.625 0 00-.975-2.446 5.54 5.54 0 012.258 2.446zM8.5 2.561c.93.276 1.759 1.457 2.175 3.107H8.5V2.561zm-1 .061v3.046H5.496C5.886 4.12 6.64 2.983 7.5 2.622zm-1.965.464a7.47 7.47 0 00-1.066 2.582H3.022a5.538 5.538 0 012.513-2.583zm-2.556 7.15h1.469a7.555 7.555 0 001.081 2.676 5.534 5.534 0 01-2.55-2.675zM7.5 13.38c-.88-.367-1.646-1.54-2.027-3.142H7.5v3.142zm1 .06v-3.202h2.197C10.291 11.94 9.45 13.16 8.5 13.439zm2.231-.665a7.71 7.71 0 00.991-2.537h1.305a5.534 5.534 0 01-2.296 2.538zM2.643 9.237a5.522 5.522 0 01.023-2.569h10.675c.21.843.217 1.723.023 2.569H2.644z"></path><path d="M6.236 8.165h-.01l-.183-.78h-.382l-.178.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.014l.191.715h.418l.32-1.227h-.392l-.16.78zM8.177 8.165h-.011l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.32 1.227h.418l.192-.715h.013l.192.715h.418l.32-1.227h-.392l-.16.78zM10.117 8.165h-.01l-.182-.78h-.383l-.177.786h-.01l-.166-.786h-.392l.319 1.227h.419l.191-.715h.014l.192.715h.418l.32-1.227h-.392l-.16.78zM10.881 8.222a.2.2 0 00-.147.06.193.193 0 00-.06.146.195.195 0 00.06.146.199.199 0 00.147.061c.036 0 .071-.01.102-.028a.22.22 0 00.075-.075.2.2 0 00.014-.184.195.195 0 00-.047-.066.2.2 0 00-.144-.06z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d="M10.72 1.5H9.265v3.198l1.455.004V1.5ZM7.36 3.347l1.516 1.517-1.032 1.025-1.513-1.513 1.03-1.029ZM4.485 6.28h3.202l-.005 1.455H4.485V6.28Zm1.848 3.36 1.515-1.516 1.026 1.032-1.512 1.512-1.03-1.029Zm2.932 2.875V9.313l1.455.005v3.197H9.265Zm3.36-1.845-1.517-1.517 1.032-1.026 1.514 1.514-1.03 1.029ZM15.5 7.735h-3.202l.005-1.455H15.5v1.455Zm-1.847-3.359-1.516 1.516-1.025-1.032 1.513-1.513 1.028 1.03ZM2 1.829v.82h-.822v1.315H2v.821h1.314v-.82h.821V2.65h-.821v-.821H2Zm0 12.842v-1.5H.5v-1.314H2v-1.499h1.314v1.499h1.5v1.314h-1.5v1.5H2Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" role="presentation" viewBox="0 0 20 20" aria-hidden="true"><path d="M10.72 1.5H9.265v3.198l1.455.004V1.5ZM7.36 3.347l1.516 1.517-1.032 1.025-1.513-1.513 1.03-1.029ZM4.485 6.28h3.202l-.005 1.455H4.485V6.28Zm1.848 3.36 1.515-1.516 1.026 1.032-1.512 1.512-1.03-1.029Zm2.932 2.875V9.313l1.455.005v3.197H9.265Zm3.36-1.845-1.517-1.517 1.032-1.026 1.514 1.514-1.03 1.029ZM15.5 7.735h-3.202l.005-1.455H15.5v1.455Zm-1.847-3.359-1.516 1.516-1.025-1.032 1.513-1.513 1.028 1.03ZM2 1.829v.82h-.822v1.315H2v.821h1.314v-.82h.821V2.65h-.821v-.821H2Zm0 12.842v-1.5H.5v-1.314H2v-1.499h1.314v1.499h1.5v1.314h-1.5v1.5H2Z"></path></svg>

Before

Width:  |  Height:  |  Size: 653 B

After

Width:  |  Height:  |  Size: 656 B

10
src/modules.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
declare module "astro-live-code" {
type LiveCodeOptions = {
layout?: string;
wrapper?: string;
defaultProps?: Record<string, any>;
};
export default function AstroLiveCode(
options?: LiveCodeOptions,
): import("astro").AstroIntegration;
}

View file

@ -1,117 +1,142 @@
import rss from '@astrojs/rss';
import rss from "@astrojs/rss";
import { getCollection, getEntry } from "astro:content";
import type { APIRoute } from 'astro';
import { marked, type Token } from 'marked';
import { getWranglerChangelog } from '~/util/changelogs';
import { slug } from "github-slugger"
import { entryToString } from '~/util/container';
import type { APIRoute } from "astro";
import { marked, type Token } from "marked";
import { getWranglerChangelog } from "~/util/changelogs";
import { slug } from "github-slugger";
import { entryToString } from "~/util/container";
export async function getStaticPaths() {
const changelogs = await getCollection("docs", (entry) => {
return entry.data.pcx_content_type === "changelog" && entry.data.changelog_file_name || entry.data.changelog_product_area_name
});
const changelogs = await getCollection("docs", (entry) => {
return (
(entry.data.pcx_content_type === "changelog" &&
entry.data.changelog_file_name) ||
entry.data.changelog_product_area_name
);
});
return changelogs.map((entry) => {
return {
params: {
changelog: entry.slug + `/index`
},
props: {
entry
}
}
})
return changelogs.map((entry) => {
return {
params: {
changelog: entry.slug + `/index`,
},
props: {
entry,
},
};
});
}
export const GET: APIRoute = async (context) => {
function walkTokens(token: Token) {
if (token.type === 'image' || token.type === 'link') {
if (token.href.startsWith("/")) {
token.href = context.site + token.href.slice(1);
}
}
}
function walkTokens(token: Token) {
if (token.type === "image" || token.type === "link") {
if (token.href.startsWith("/")) {
token.href = context.site + token.href.slice(1);
}
}
}
marked.use({ walkTokens });
marked.use({ walkTokens });
const entry = context.props.entry;
const entry = context.props.entry;
if (!entry.data.changelog_file_name && !entry.data.changelog_product_area_name) {
throw new Error(`One of changelog_file_name or changelog_product_area_name is required on ${entry.id}, to generate RSS feeds.`)
}
if (
!entry.data.changelog_file_name &&
!entry.data.changelog_product_area_name
) {
throw new Error(
`One of changelog_file_name or changelog_product_area_name is required on ${entry.id}, to generate RSS feeds.`,
);
}
const changelogs = await getCollection("changelogs", (changelog) => {
return entry.data.changelog_file_name?.includes(changelog.id) || changelog.data.productArea === entry.data.changelog_product_area_name
})
const changelogs = await getCollection("changelogs", (changelog) => {
return (
entry.data.changelog_file_name?.includes(changelog.id) ||
changelog.data.productArea === entry.data.changelog_product_area_name
);
});
if (entry.data.changelog_file_name?.includes("wrangler")) {
changelogs.push(await getWranglerChangelog());
}
if (entry.data.changelog_file_name?.includes("wrangler")) {
changelogs.push(await getWranglerChangelog());
}
const mapped = await Promise.all(changelogs.flatMap((product) => {
return product.data.entries.map(async (entry) => {
let description;
if (entry.individual_page) {
const link = entry.link;
const mapped = await Promise.all(
changelogs.flatMap((product) => {
return product.data.entries.map(async (entry) => {
let description;
if (entry.individual_page) {
const link = entry.link;
const page = await getEntry("docs", link.slice(1, -1));
if (!link)
throw new Error(
`Changelog entry points to individual page but no link is provided`,
);
if (!page) throw new Error(`Changelog entry points to ${link.slice(1, -1)} but unable to find entry with that slug`)
const page = await getEntry("docs", link.slice(1, -1));
description = await entryToString(page) ?? page.body;
} else {
description = entry.description;
}
if (!page)
throw new Error(
`Changelog entry points to ${link.slice(1, -1)} but unable to find entry with that slug`,
);
let link;
if (entry.link) {
link = entry.link
} else {
const anchor = slug(entry.title ?? entry.publish_date);
link = product.data.link.concat(`#${anchor}`);
}
description = (await entryToString(page)) ?? page.body;
} else {
description = entry.description;
}
let title;
if (entry.scheduled) {
title = `Scheduled for ${entry.scheduled_date}`
} else {
title = entry.title;
}
let link;
if (entry.link) {
link = entry.link;
} else {
const anchor = slug(entry.title ?? entry.publish_date);
link = product.data.link.concat(`#${anchor}`);
}
return {
product: product.data.productName,
link,
date: entry.publish_date,
description,
title,
};
});
}));
let title;
if (entry.scheduled) {
title = `Scheduled for ${entry.scheduled_date}`;
} else {
title = entry.title;
}
const entries = mapped.sort((a, b) => {
return (a.date < b.date) ? 1 : ((a.date > b.date) ? -1 : 0);
});
return {
product: product.data.productName,
link,
date: entry.publish_date,
description,
title,
};
});
}),
);
const rssName = entry.data.changelog_product_area_name || changelogs[0].data.productName;
const entries = mapped.sort((a, b) => {
return a.date < b.date ? 1 : a.date > b.date ? -1 : 0;
});
const site = new URL(context.site);
site.pathname = entry.slug.concat("/");
const rssName =
entry.data.changelog_product_area_name || changelogs[0].data.productName;
const isArea = Boolean(entry.data.changelog_product_area_name);
const site = new URL(context.site ?? "");
site.pathname = entry.slug.concat("/");
return rss({
title: `Changelog | ${rssName}`,
description: `Updates to ${rssName}`,
site,
trailingSlash: false,
items: entries.map((entry) => {
return {
title: `${entry.product} - ${entry.title ?? entry.date}`,
description: marked.parse(entry.description),
pubDate: new Date(entry.date),
link: entry.link,
customData: isArea ? `<product>${entry.product}</product>` : undefined,
}
})
})
}
const isArea = Boolean(entry.data.changelog_product_area_name);
return rss({
title: `Changelog | ${rssName}`,
description: `Updates to ${rssName}`,
site,
trailingSlash: false,
items: entries.map((entry) => {
return {
title: `${entry.product} - ${entry.title ?? entry.date}`,
description: marked.parse(entry.description ?? "", {
async: false,
}) as string,
pubDate: new Date(entry.date),
link: entry.link,
customData: isArea ? `<product>${entry.product}</product>` : undefined,
};
}),
});
};

View file

@ -9,39 +9,76 @@ const { products, productAreas, changelogs } = await getChangelogs();
<StarlightPage frontmatter={{ title: "Changelogs", template: "splash" }}>
<Badge text="Beta" variant="caution" size="medium" />
<br/>
<br/>
<p id="productDescription">Subscribe to all Changelog posts via <a href="/changelog/index.xml">RSS</a>.</p>
<p id="productAreaDescription" style="display:none">Subscribe to all Changelog posts via <a class="rssLink" href="/changelog/index.xml">RSS</a>.</p>
<p>Unless otherwise noted, all dates refer to the release date of the change.</p>
<br/>
<br />
<br />
<p id="productDescription">
Subscribe to all Changelog posts via <a href="/changelog/index.xml">RSS</a>.
</p>
<p id="productAreaDescription" style="display:none">
Subscribe to all Changelog posts via <a
class="rssLink"
href="/changelog/index.xml">RSS</a
>.
</p>
<p>
Unless otherwise noted, all dates refer to the release date of the change.
</p>
<br />
<label for="products">Product:</label>
<select name="products" id="products">
<option value="all">Select...</option>
{products.map((product) => <option value={product.toLowerCase()}>{product}</option>)}
{
products.map((product) => (
<option value={product.toLowerCase()}>{product}</option>
))
}
</select>
<label for="productAreas">Product area:</label>
<select name="productAreas" id="productAreas">
<option value="all">Select...</option>
{productAreas.map((productAreas) => <option value={productAreas.toLowerCase()}>{productAreas}</option>)}
{
productAreas.map((productAreas) => (
<option value={productAreas.toLowerCase()}>{productAreas}</option>
))
}
</select>
{
changelogs.map(([date, entries]) => (
<div style="overflow-anchor: none;" data-date={date}>
<h2>{date}</h2>
{entries?.map((entry) => (
<div data-product={entry.product.toLowerCase()} data-productArea={entry.productAreaName.toLowerCase()}>
<div
data-product={entry.product.toLowerCase()}
data-productArea={entry.productAreaName.toLowerCase()}
>
<h3>
<a href={entry.link}>{entry.product}</a>
</h3>
{["WAF", "DDoS protection"].includes(entry.product) ? (
<p set:html={marked.parse((entry.scheduled ? "**" + "Scheduled changes for " + (entry.date ?? "") + "**" : "**" + (entry.date ?? "") + "**"))}></p>
<p
set:html={marked.parse(
entry.scheduled
? "**" +
"Scheduled changes for " +
(entry.date ?? "") +
"**"
: "**" + (entry.date ?? "") + "**",
)}
/>
) : (
<p set:html={marked.parse("**" + (entry.title ?? "") + "**")} />
)}
{["WAF", "DDoS protection"].includes(entry.product) ? (<p set:html={marked.parse("For more details, refer to the [changelog page](" + entry.link + ").")}></p>
) : (
<p set:html={marked.parse(entry.description ?? "")} />)}
{["WAF", "DDoS protection"].includes(entry.product) ? (
<p
set:html={marked.parse(
"For more details, refer to the [changelog page](" +
entry.link +
").",
)}
/>
) : (
<p set:html={marked.parse(entry.description ?? "")} />
)}
</div>
))}
</div>
@ -50,20 +87,25 @@ const { products, productAreas, changelogs } = await getChangelogs();
</StarlightPage>
<script>
import StickyHeader from '@codingheads/sticky-header';
import StickyHeader from "@codingheads/sticky-header";
document.addEventListener("DOMContentLoaded", () => {
const navHeightRem = getComputedStyle(document.body).getPropertyValue('--sl-nav-height');
const navHeightRem = getComputedStyle(document.body).getPropertyValue(
"--sl-nav-height",
);
const navHeightPx = Number(navHeightRem.split("rem")[0]) * 16 + 16;
const headers = document.querySelectorAll<HTMLElement>("[data-date] > h2");
headers.forEach((header) => new StickyHeader(header, { offset: 0 - navHeightPx }));
headers.forEach(
(header) => new StickyHeader(header, { offset: 0 - navHeightPx }),
);
});
const productFilter = document.querySelector<HTMLSelectElement>("#products");
productFilter?.addEventListener("change", filterEntries);
const productAreaFilter = document.querySelector<HTMLSelectElement>("#productAreas");
const productAreaFilter =
document.querySelector<HTMLSelectElement>("#productAreas");
productAreaFilter?.addEventListener("change", filterEntries);
function filterEntries() {
@ -72,7 +114,10 @@ const { products, productAreas, changelogs } = await getChangelogs();
for (const date of dates) {
const entries = date.querySelectorAll<HTMLElement>("[data-product]");
if (productAreaFilter.value === "all" && productFilter.value === "all") {
if (
productAreaFilter?.value === "all" &&
productFilter?.value === "all"
) {
dates.forEach((x) => {
x.style.display = "";
});
@ -86,15 +131,19 @@ const { products, productAreas, changelogs } = await getChangelogs();
for (const entry of entries) {
const product = entry.dataset.product;
const productArea = entry.dataset.productarea
const productArea = entry.dataset.productarea;
if ((productFilter?.value === product || productFilter?.value === "all") && (productAreaFilter?.value === productArea || productAreaFilter?.value === "all") ) {
if (
(productFilter?.value === product ||
productFilter?.value === "all") &&
(productAreaFilter?.value === productArea ||
productAreaFilter?.value === "all")
) {
entry.style.display = "";
date.style.display = "";
} else {
entry.style.display = "none";
entriesHidden++;
}
}

View file

@ -1,83 +1,95 @@
import rss from '@astrojs/rss';
import rss from "@astrojs/rss";
import { getCollection, getEntry } from "astro:content";
import type { APIRoute } from 'astro';
import { marked } from 'marked';
import { getWranglerChangelog } from '~/util/changelogs';
import { slug } from "github-slugger"
import { entryToString } from '~/util/container';
import type { APIRoute } from "astro";
import { marked, type Token } from "marked";
import { getWranglerChangelog } from "~/util/changelogs";
import { slug } from "github-slugger";
import { entryToString } from "~/util/container";
export const GET: APIRoute = async (context) => {
function walkTokens(token: Token) {
if (token.type === 'image' || token.type === 'link') {
if (token.href.startsWith("/")) {
token.href = context.site + token.href.slice(1);
}
}
}
function walkTokens(token: Token) {
if (token.type === "image" || token.type === "link") {
if (token.href.startsWith("/")) {
token.href = context.site + token.href.slice(1);
}
}
}
marked.use({ walkTokens });
marked.use({ walkTokens });
const changelogs = await getCollection("changelogs")
const changelogs = await getCollection("changelogs");
changelogs.push(await getWranglerChangelog());
changelogs.push(await getWranglerChangelog());
const mapped = await Promise.all(changelogs.flatMap((product) => {
return product.data.entries.map(async (entry) => {
let description;
if (entry.individual_page) {
const link = entry.link;
const mapped = await Promise.all(
changelogs.flatMap((product) => {
return product.data.entries.map(async (entry) => {
let description;
if (entry.individual_page) {
const link = entry.link;
const page = await getEntry("docs", link.slice(1, -1));
if (!link)
throw new Error(
`Changelog entry points to individual page but no link is provided`,
);
if (!page) throw new Error(`Changelog entry points to ${link.slice(1, -1)} but unable to find entry with that slug`)
const page = await getEntry("docs", link.slice(1, -1));
description = await entryToString(page) ?? page.body;
} else {
description = entry.description;
}
if (!page)
throw new Error(
`Changelog entry points to ${link.slice(1, -1)} but unable to find entry with that slug`,
);
let link;
if (entry.link) {
link = entry.link
} else {
const anchor = slug(entry.title ?? entry.publish_date);
link = product.data.link.concat(`#${anchor}`);
}
description = (await entryToString(page)) ?? page.body;
} else {
description = entry.description;
}
let title;
if (entry.scheduled) {
title = `Scheduled for ${entry.scheduled_date}`
} else {
title = entry.title;
}
let link;
if (entry.link) {
link = entry.link;
} else {
const anchor = slug(entry.title ?? entry.publish_date);
link = product.data.link.concat(`#${anchor}`);
}
return {
product: product.data.productName,
link,
date: entry.publish_date,
description,
title,
};
});
}));
let title;
if (entry.scheduled) {
title = `Scheduled for ${entry.scheduled_date}`;
} else {
title = entry.title;
}
const entries = mapped.sort((a, b) => {
return (a.date < b.date) ? 1 : ((a.date > b.date) ? -1 : 0);
});
return {
product: product.data.productName,
link,
date: entry.publish_date,
description,
title,
};
});
}),
);
return rss({
title: `Cloudflare product changelog`,
description: `Updates to various Cloudflare products.`,
site: "https://developers.cloudflare.com/changelog/",
trailingSlash: false,
items: entries.map((entry) => {
return {
title: `${entry.product} - ${entry.title ?? entry.date}`,
description: marked.parse(entry.description),
pubDate: new Date(entry.date),
link: entry.link,
customData: `<product>${entry.product}</product>`
}
})
})
}
const entries = mapped.sort((a, b) => {
return a.date < b.date ? 1 : a.date > b.date ? -1 : 0;
});
return rss({
title: `Cloudflare product changelog`,
description: `Updates to various Cloudflare products.`,
site: "https://developers.cloudflare.com/changelog/",
trailingSlash: false,
items: entries.map((entry) => {
return {
title: `${entry.product} - ${entry.title ?? entry.date}`,
description: marked.parse(entry.description ?? "", {
async: false,
}) as string,
pubDate: new Date(entry.date),
link: entry.link,
customData: `<product>${entry.product}</product>`,
};
}),
});
};

View file

@ -1,11 +1,11 @@
---
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import { Glossary } from "~/components";
import { Glossary as GlossaryComponent } from "~/components";
import { Badge } from "~/components";
---
<StarlightPage frontmatter={{ title: "Glossary", template: "splash" }}>
<Badge text="Beta" variant="caution" size="medium" />
<br/>
<Glossary/>
<br />
<GlossaryComponent />
</StarlightPage>

View file

@ -10,245 +10,257 @@ import SectionImageZeroTrustDark from "~/assets/zero-trust-dark.svg";
import SectionImageZeroTrustLight from "~/assets/zero-trust-light.svg";
import {
FourCardGrid,
ListCard,
TryItSection,
FeaturedContentSection,
RecommendedContentSection,
FooterHeroBlock,
FourCardGrid,
ListCard,
TryItSection,
FeaturedContentSection,
RecommendedContentSection,
FooterHeroBlock,
} from "~/components";
const frontmatter = {
title: "Welcome to Cloudflare",
description:
"Explore guides and tutorials to start building on Cloudflare's platform",
template: "splash",
editUrl: false,
hero: {
title: "Welcome to Cloudflare",
tagline:
"Explore guides and tutorials to start building on Cloudflare's platform",
image: {
dark: HeroImageDark,
light: HeroImageLight,
},
},
};
title: "Welcome to Cloudflare",
description:
"Explore guides and tutorials to start building on Cloudflare's platform",
template: "splash",
hero: {
title: "Welcome to Cloudflare",
tagline:
"Explore guides and tutorials to start building on Cloudflare's platform",
image: {
dark: HeroImageDark,
light: HeroImageLight,
},
},
} as const;
const topCards = [
{
title: "Featured",
links: [
{ text: "Add web analytics", href: "/web-analytics/" },
{
text: "Troubleshoot errors",
href: "/support/troubleshooting/cloudflare-errors/",
},
{ text: "Register a domain", href: "/registrar/" },
{ text: "Setup 1.1.1.1", href: "/1.1.1.1/setup/" },
{
text: "Get started with Cloudflare",
href: "/learning-paths/get-started/",
},
],
cta: {
text: "View all products",
href: "/products/",
},
},
{
title: "Developer Products",
links: [
{ text: "Workers", href: "/workers/" },
{ text: "Pages", href: "/pages/" },
{ text: "R2", href: "/r2/" },
{ text: "Images", href: "/images/" },
{ text: "Stream", href: "/stream/" },
],
cta: {
text: "View all developer products",
href: "/products/?product-group=Developer+platform",
},
},
{
title: "AI Products",
links: [
{
text: "Build a RAG",
href: "/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",
},
{ text: "Workers AI", href: "/workers-ai/" },
{ text: "Vectorize", href: "/vectorize/" },
{ text: "AI Gateway", href: "/ai-gateway/" },
{ text: "AI Playground", href: "https://playground.ai.cloudflare.com/", target: "_blank" },
],
cta: {
text: "View all AI products",
href: "/products/?product-group=AI",
},
},
{
title: "Zero Trust",
links: [
{ text: "Access", href: "/cloudflare-one/policies/access/" },
{ text: "Tunnel", href: "/cloudflare-one/connections/connect-networks/" },
{ text: "Gateway", href: "/cloudflare-one/policies/gateway/" },
{ text: "Browser Isolation", href: "/cloudflare-one/policies/browser-isolation/" },
{ text: "Replace your VPN", href: "/learning-paths/replace-vpn/" },
],
cta: {
text: "View all Cloudflare One products",
href: "/products/?product-group=Cloudflare+One",
},
},
{
title: "Featured",
links: [
{ text: "Add web analytics", href: "/web-analytics/" },
{
text: "Troubleshoot errors",
href: "/support/troubleshooting/cloudflare-errors/",
},
{ text: "Register a domain", href: "/registrar/" },
{ text: "Setup 1.1.1.1", href: "/1.1.1.1/setup/" },
{
text: "Get started with Cloudflare",
href: "/learning-paths/get-started/",
},
],
cta: {
text: "View all products",
href: "/products/",
},
},
{
title: "Developer Products",
links: [
{ text: "Workers", href: "/workers/" },
{ text: "Pages", href: "/pages/" },
{ text: "R2", href: "/r2/" },
{ text: "Images", href: "/images/" },
{ text: "Stream", href: "/stream/" },
],
cta: {
text: "View all developer products",
href: "/products/?product-group=Developer+platform",
},
},
{
title: "AI Products",
links: [
{
text: "Build a RAG",
href: "/workers-ai/tutorials/build-a-retrieval-augmented-generation-ai/",
},
{ text: "Workers AI", href: "/workers-ai/" },
{ text: "Vectorize", href: "/vectorize/" },
{ text: "AI Gateway", href: "/ai-gateway/" },
{
text: "AI Playground",
href: "https://playground.ai.cloudflare.com/",
target: "_blank",
},
],
cta: {
text: "View all AI products",
href: "/products/?product-group=AI",
},
},
{
title: "Zero Trust",
links: [
{ text: "Access", href: "/cloudflare-one/policies/access/" },
{ text: "Tunnel", href: "/cloudflare-one/connections/connect-networks/" },
{ text: "Gateway", href: "/cloudflare-one/policies/gateway/" },
{
text: "Browser Isolation",
href: "/cloudflare-one/policies/browser-isolation/",
},
{ text: "Replace your VPN", href: "/learning-paths/replace-vpn/" },
],
cta: {
text: "View all Cloudflare One products",
href: "/products/?product-group=Cloudflare+One",
},
},
];
const featuredSections = [
{
title: "Developer Platform",
text: "The Cloudflare Developer Platform provides a serverless execution environment that allows you to create entirely new applications or augment existing ones without configuring or maintaining infrastructure.",
image: {
light: SectionImageDeveloperPlatformLight,
dark: SectionImageDeveloperPlatformDark,
},
imagePosition: "before",
cards: [
{
title: "Create API Tokens",
text: "If you are going to be using the Cloudflare API, you first need an API token to authenticate your requests.",
cta: {
title: "Create Tokens",
href: "/fundamentals/api/get-started/create-token/",
},
},
{
title: "View Workers Examples",
text: "Review fully functional sample scripts to get started with Workers.",
cta: {
title: "View Examples",
href: "/workers/examples/",
},
},
],
},
{
title: "Zero Trust",
text: "Cloudflare Zero Trust replaces legacy security perimeters with our global network, making the Internet faster and safer for teams around the world.",
image: {
light: SectionImageZeroTrustLight,
dark: SectionImageZeroTrustDark,
},
imagePosition: "after",
cards: [
{
title: "Install the WARP Client",
text: "The Cloudflare WARP client allows individuals and organizations to have a faster, more secure, and more private experience online.",
cta: {
title: "Get started",
href: "/cloudflare-one/connections/connect-devices/warp/",
},
},
{
title: "Set up a tunnel",
text: "Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address.",
cta: {
title: "Set up a tunnel",
href: "/cloudflare-one/connections/connect-networks/",
},
},
],
},
{
title: "Developer Platform",
text: "The Cloudflare Developer Platform provides a serverless execution environment that allows you to create entirely new applications or augment existing ones without configuring or maintaining infrastructure.",
image: {
light: SectionImageDeveloperPlatformLight,
dark: SectionImageDeveloperPlatformDark,
},
imagePosition: "before",
cards: [
{
title: "Create API Tokens",
text: "If you are going to be using the Cloudflare API, you first need an API token to authenticate your requests.",
cta: {
title: "Create Tokens",
href: "/fundamentals/api/get-started/create-token/",
},
},
{
title: "View Workers Examples",
text: "Review fully functional sample scripts to get started with Workers.",
cta: {
title: "View Examples",
href: "/workers/examples/",
},
},
],
},
{
title: "Zero Trust",
text: "Cloudflare Zero Trust replaces legacy security perimeters with our global network, making the Internet faster and safer for teams around the world.",
image: {
light: SectionImageZeroTrustLight,
dark: SectionImageZeroTrustDark,
},
imagePosition: "after",
cards: [
{
title: "Install the WARP Client",
text: "The Cloudflare WARP client allows individuals and organizations to have a faster, more secure, and more private experience online.",
cta: {
title: "Get started",
href: "/cloudflare-one/connections/connect-devices/warp/",
},
},
{
title: "Set up a tunnel",
text: "Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address.",
cta: {
title: "Set up a tunnel",
href: "/cloudflare-one/connections/connect-networks/",
},
},
],
},
];
const recommendedSection = {
title: "Other docs you might also like",
cards: [
{
title: "Install an Origin CA certificate",
text: "Use Origin Certificate Authority (CA) certificates to encrypt traffic between Cloudflare and your origin web server and reduce origin bandwidth.",
cta: {
title: "Install Origin CA",
href: "/ssl/origin-configuration/origin-ca/",
},
},
{
title: "Change your nameservers",
text: "Make Cloudflare your primary DNS provider by updating your authoritative nameservers at your domain registrar.",
cta: {
title: "Update nameservers",
href: "/dns/zone-setups/full-setup/setup/",
},
},
{
title: "SSL/TLS Encryption mode",
text: "Your domain's encryption mode controls how Cloudflare connects to your origin server and how SSL certificates at your origin will be validated.",
cta: {
title: "Set encryption mode",
href: "/ssl/origin-configuration/ssl-modes/",
},
},
{
title: "Allow traffic from specific countries only",
text: "Block requests based on a list of allowed countries by configuring a custom rule in the Web Application Firewall (WAF).",
cta: {
title: "Allow traffic from specific countries only",
href: "/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",
},
},
],
title: "Other docs you might also like",
cards: [
{
title: "Install an Origin CA certificate",
text: "Use Origin Certificate Authority (CA) certificates to encrypt traffic between Cloudflare and your origin web server and reduce origin bandwidth.",
cta: {
title: "Install Origin CA",
href: "/ssl/origin-configuration/origin-ca/",
},
},
{
title: "Change your nameservers",
text: "Make Cloudflare your primary DNS provider by updating your authoritative nameservers at your domain registrar.",
cta: {
title: "Update nameservers",
href: "/dns/zone-setups/full-setup/setup/",
},
},
{
title: "SSL/TLS Encryption mode",
text: "Your domain's encryption mode controls how Cloudflare connects to your origin server and how SSL certificates at your origin will be validated.",
cta: {
title: "Set encryption mode",
href: "/ssl/origin-configuration/ssl-modes/",
},
},
{
title: "Allow traffic from specific countries only",
text: "Block requests based on a list of allowed countries by configuring a custom rule in the Web Application Firewall (WAF).",
cta: {
title: "Allow traffic from specific countries only",
href: "/waf/custom-rules/use-cases/allow-traffic-from-specific-countries/",
},
},
],
};
---
<StarlightPage frontmatter={frontmatter}>
<FourCardGrid>
{
topCards.map((card) => (
<ListCard title={card.title}>
<ul>
{card.links.map((link) => (
<li>
<a href={link.href} target={link.target} class="!text-black hover:!text-accent-600 dark:!text-accent-200 dark:hover:!text-white">{link.text}</a>
</li>
))}
</ul>
<p>
<strong>
<a href={card.cta.href}>{card.cta.text}</a>
</strong>
</p>
</ListCard>
))
}
</FourCardGrid>
<div class="hidden md:block">
<hr />
<TryItSection />
<hr />
{
featuredSections.map((section) => (
<>
<FeaturedContentSection
title={section.title}
text={section.text}
image={section.image}
imagePosition={section.imagePosition}
cards={section.cards}
/>
<hr />
</>
))
}
<RecommendedContentSection
title={recommendedSection.title}
cards={recommendedSection.cards}
/>
<hr />
<FooterHeroBlock />
</div>
<FourCardGrid>
{
topCards.map((card) => (
<ListCard title={card.title}>
<ul>
{card.links.map((link) => (
<li>
<a
href={link.href}
target={link.target}
class="!text-black hover:!text-accent-600 dark:!text-accent-200 dark:hover:!text-white"
>
{link.text}
</a>
</li>
))}
</ul>
<p>
<strong>
<a href={card.cta.href}>{card.cta.text}</a>
</strong>
</p>
</ListCard>
))
}
</FourCardGrid>
<div class="hidden md:block">
<hr />
<TryItSection />
<hr />
{
featuredSections.map((section) => (
<>
<FeaturedContentSection
title={section.title}
text={section.text}
image={section.image}
imagePosition={section.imagePosition}
cards={section.cards}
/>
<hr />
</>
))
}
<RecommendedContentSection
title={recommendedSection.title}
cards={recommendedSection.cards}
/>
<hr />
<FooterHeroBlock />
</div>
</StarlightPage>
<style>
html:not([data-has-sidebar]) {
--sl-content-width: 80rem;
}
</style>
html:not([data-has-sidebar]) {
--sl-content-width: 80rem;
}
</style>

View file

@ -9,7 +9,7 @@ const frontmatter = {
description:
"Learning paths guide you through modules and projects so you can get started with Cloudflare as quickly as possible.",
template: "splash",
};
} as const;
const learningPaths = await getCollection("learning-paths");
---

View file

@ -1,15 +1,16 @@
import { getCollection } from "astro:content";
export async function GET() {
const entries = await getCollection("pages-build-environment");
const entries = await getCollection("pages-build-environment");
const data = entries.flatMap(x => {
x.data.enable_date = new Date(x.data.enable_date).toISOString();
const data = entries.flatMap((x) => {
x.data.enable_date = new Date(x.data.enable_date).toISOString();
x.data.status ??= null;
return x.data
});
return {
...x.data,
status: x.data.status ?? null,
};
});
return Response.json(data);
}
return Response.json(data);
}

View file

@ -13,7 +13,7 @@ const frontmatter = {
description:
"API reference, how-to guides, tutorials, example code, and more.",
template: "splash",
};
} as const;
function productGroups(products: Array<CollectionEntry<"products">>) {
const groups = products.flatMap((p) => p.data.product.group ?? []);

View file

@ -34,15 +34,18 @@ import "instantsearch.css/themes/satellite.css";
// Functions needed for dropdowns, pulled from https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-dropdown/js/
function hasClassName(elem: HTMLElement, className: string) {
function hasClassName(elem: HTMLElement | null, className: string) {
if (!elem) return;
return elem.className.split(" ").indexOf(className) >= 0;
}
function addClassName(elem: HTMLElement, className: string) {
function addClassName(elem: HTMLElement | null, className: string) {
if (!elem) return;
elem.className = [...elem.className.split(" "), className].join(" ");
}
function removeClassName(elem: HTMLElement, className: string) {
function removeClassName(elem: HTMLElement | null, className: string) {
if (!elem) return;
elem.className = elem.className
.split(" ")
.filter((name) => name !== className)
@ -61,12 +64,17 @@ import "instantsearch.css/themes/satellite.css";
const cx = (...args: string[]) => args.filter(Boolean).join(" ");
export function createDropdown(
baseWidget,
baseWidget: any,
{
cssClasses: userCssClasses = {},
buttonText,
buttonClassName,
closeOnChange,
}: {
cssClasses?: Record<string, string>;
buttonText?: string;
buttonClassName?: string | ((options: any) => string);
closeOnChange?: boolean | (() => boolean);
} = {},
) {
// Merge class names with the default ones and the ones from user
@ -79,7 +87,7 @@ import "instantsearch.css/themes/satellite.css";
),
closeButton: cx(CLASS_CLOSE_BUTTON, userCssClasses.closeButton),
};
const makeWidget = panel({
const makeWidget = panel<typeof refinementList>({
cssClasses,
templates: {
header: (options) => {
@ -121,10 +129,14 @@ import "instantsearch.css/themes/satellite.css";
},
})(baseWidget);
return (widgetParams) => {
return (widgetParams: any) => {
const widget = makeWidget(widgetParams);
let state = {};
let rootElem, headerElem, closeButtonElem;
let state: {
windowClickListener?: (event: MouseEvent) => void;
} = {};
let rootElem: HTMLElement | null;
let headerElem: HTMLElement | null;
let closeButtonElem: HTMLButtonElement | null;
const open = () => {
addClassName(rootElem, CLASS_OPENED);
@ -134,7 +146,7 @@ import "instantsearch.css/themes/satellite.css";
setTimeout(() => {
state.windowClickListener = (event) => {
// Close if the outside is clicked
if (!event.composedPath().includes(rootElem)) {
if (rootElem && !event.composedPath().includes(rootElem)) {
close();
}
};
@ -145,7 +157,9 @@ import "instantsearch.css/themes/satellite.css";
const close = () => {
removeClassName(rootElem, CLASS_OPENED);
// Remove the event listener when the panel is closed
window.removeEventListener("click", state.windowClickListener);
if (state.windowClickListener) {
window.removeEventListener("click", state.windowClickListener);
}
delete state.windowClickListener;
};
const isOpened = () => hasClassName(rootElem, CLASS_OPENED);
@ -158,10 +172,10 @@ import "instantsearch.css/themes/satellite.css";
};
// Add a click listener to the header (button) and the caret symbol
const buttonListener = (event) => {
const buttonListener = (event: MouseEvent) => {
if (
!event.target.matches("." + CLASS_BUTTON) &&
!event.target.matches(".caretDownFilter")
!(event.target as HTMLElement)?.matches("." + CLASS_BUTTON) &&
!(event.target as HTMLElement)?.matches(".caretDownFilter")
) {
return;
}
@ -170,7 +184,7 @@ import "instantsearch.css/themes/satellite.css";
// Setup a clean-up function, which will be called in `dispose`.
const cleanUp = () => {
headerElem.removeEventListener("click", buttonListener);
headerElem?.removeEventListener("click", buttonListener);
if (state.windowClickListener) {
window.removeEventListener("click", state.windowClickListener);
}
@ -180,7 +194,7 @@ import "instantsearch.css/themes/satellite.css";
return {
...widget,
$$widgetType: "cmty.facetDropdown",
render: (options) => {
render: (options: any) => {
if (!rootElem) {
rootElem = document
.querySelector(widgetParams.container)
@ -188,15 +202,16 @@ import "instantsearch.css/themes/satellite.css";
}
if (!headerElem) {
headerElem = rootElem.querySelector(".ais-Panel-header");
headerElem = rootElem?.querySelector(".ais-Panel-header") ?? null;
headerElem.addEventListener("click", buttonListener);
headerElem?.addEventListener("click", buttonListener);
}
if (!closeButtonElem) {
closeButtonElem = rootElem.querySelector("." + CLASS_CLOSE_BUTTON);
closeButtonElem =
rootElem?.querySelector("." + CLASS_CLOSE_BUTTON) ?? null;
closeButtonElem.addEventListener("click", close);
closeButtonElem?.addEventListener("click", close);
}
// Whenever uiState changes, it closes the panel.
@ -217,7 +232,7 @@ import "instantsearch.css/themes/satellite.css";
return widget.render.call(widget, options);
},
dispose: (options) => {
dispose: (options: any) => {
if (typeof cleanUp === "function") {
cleanUp();
}
@ -236,6 +251,7 @@ import "instantsearch.css/themes/satellite.css";
const search = instantsearch({
indexName: indexName,
// @ts-expect-error TODO: improve types
searchClient,
insights: true,
routing: {
@ -257,6 +273,7 @@ import "instantsearch.css/themes/satellite.css";
indexUiState.refinementList.content_type,
};
},
// @ts-expect-error TODO: improve types
routeToState(routeState) {
return {
[indexName]: {
@ -355,6 +372,7 @@ import "instantsearch.css/themes/satellite.css";
},
}),
configure({
// @ts-expect-error TODO: improve types
hitsPerPage: 10,
attributesToSnippet: ["content:30"],
}),

View file

@ -28,6 +28,7 @@ import TranslationResponses from "~/components/models/responses/TranslationRespo
import TranslationCode from "~/components/models/code/TranslationCode.astro";
import StableDiffusionV15Img2ImgCode from "~/components/models/code/StableDiffusion-v1-5-img2imgCode.astro";
import StableDiffusionV15InpaintingCode from "~/components/models/code/StableDiffusion-v1-5-inpaintingCode.astro";
import type { ComponentProps } from "astro/types";
export const getStaticPaths = (async () => {
const models = await getCollection("workers-ai-models");
@ -52,106 +53,118 @@ const hasLora = Boolean(
data.model.properties.find((x) => x.property_id === "lora"),
);
const badges = data.model.properties.flatMap(({ property_id, value }) => {
if (property_id === "beta" && value === "true") {
return {
preset: "beta",
};
}
type InlineBadgeProps = ComponentProps<typeof InlineBadge>;
if (property_id === "lora" && value === "true") {
return {
variant: "tip",
text: "LoRA",
};
}
if (property_id === "function_calling" && value === "true") {
return {
variant: "note",
text: "Function calling",
};
}
if (property_id === "planned_deprecation_date") {
const timestamp = Math.floor(new Date(value).getTime() / 1000);
if (Date.now() > timestamp) {
return { variant: "danger", text: "Deprecated" };
const badges = data.model.properties
.flatMap(({ property_id, value }): InlineBadgeProps | null => {
if (property_id === "beta" && value === "true") {
return {
preset: "beta",
text: "Beta",
} as const;
}
return { variant: "danger", text: "Planned deprecation" };
}
if (property_id === "lora" && value === "true") {
return {
variant: "tip",
text: "LoRA",
} as const;
}
return [];
});
if (property_id === "function_calling" && value === "true") {
return {
variant: "note",
text: "Function calling",
} as const;
}
let CodeExamples;
let Responses;
if (property_id === "planned_deprecation_date") {
const timestamp = Math.floor(new Date(value).getTime() / 1000);
switch (data.task_type) {
case "text-generation": {
CodeExamples = TextGenerationCode;
Responses = TextGenerationResponses;
if (Date.now() > timestamp) {
return { variant: "danger", text: "Deprecated" } as const;
}
return { variant: "danger", text: "Planned deprecation" } as const;
}
return null;
})
.filter(Boolean) as InlineBadgeProps[];
function getComponents(info: typeof data) {
switch (info.task_type) {
case "text-generation": {
return {
CodeExamples: TextGenerationCode,
Responses: TextGenerationResponses,
};
}
case "automatic-speech-recognition":
return {
CodeExamples: AutomaticSpeechRecognitionCode,
Responses: AutomaticSpeechRecognitionResponses,
};
case "image-classification":
return {
CodeExamples: ImageClassificationCode,
Responses: ImageClassificationResponses,
};
case "image-to-text":
return {
CodeExamples: ImageToTextCode,
Responses: ImageToTextResponses,
};
case "object-detection":
return {
CodeExamples: ObjectDetectionCode,
Responses: ObjectDetectionResponses,
};
case "summarization":
return {
CodeExamples: SummarizationCode,
Responses: SummarizationResponses,
};
case "text-classification":
return {
CodeExamples: TextClassificationCode,
Responses: TextClassificationResponses,
};
case "text-embeddings":
return {
CodeExamples: TextEmbeddingCode,
Responses: TextEmbeddingsResponses,
};
case "text-to-image":
return {
CodeExamples: TextToImageCode,
Responses: TextToImageResponses,
};
case "translation":
return {
CodeExamples: TranslationCode,
Responses: TranslationResponses,
};
}
break;
case "automatic-speech-recognition": {
CodeExamples = AutomaticSpeechRecognitionCode;
Responses = AutomaticSpeechRecognitionResponses;
}
break;
case "image-classification": {
CodeExamples = ImageClassificationCode;
Responses = ImageClassificationResponses;
}
break;
case "image-to-text": {
CodeExamples = ImageToTextCode;
Responses = ImageToTextResponses;
}
break;
case "object-detection": {
CodeExamples = ObjectDetectionCode;
Responses = ObjectDetectionResponses;
}
break;
case "summarization": {
CodeExamples = SummarizationCode;
Responses = SummarizationResponses;
}
break;
case "text-classification": {
CodeExamples = TextClassificationCode;
Responses = TextClassificationResponses;
}
break;
case "text-embeddings": {
CodeExamples = TextEmbeddingCode;
Responses = TextEmbeddingsResponses;
}
break;
case "text-to-image": {
CodeExamples = TextToImageCode;
Responses = TextToImageResponses;
}
break;
case "translation": {
CodeExamples = TranslationCode;
Responses = TranslationResponses;
}
break;
// default
return {
CodeExamples: null,
Responses: null,
};
}
const components = getComponents(data);
const CodeExamples = components.CodeExamples;
const Responses = components.Responses;
if (data.model.name === "@cf/runwayml/stable-diffusion-v1-5-inpainting") {
CodeExamples = StableDiffusionV15InpaintingCode;
components.CodeExamples = StableDiffusionV15InpaintingCode;
} else if (data.model.name === "@cf/runwayml/stable-diffusion-v1-5-img2img") {
CodeExamples = StableDiffusionV15Img2ImgCode;
components.CodeExamples = StableDiffusionV15Img2ImgCode;
}
---
<StarlightPage frontmatter={{ title: name, description }}>
{badges.map((badge) => <InlineBadge {...badge} /> )}
{badges.map((badge) => <InlineBadge {...badge} />)}
<p>
<strong>Model ID: </strong>
<code>{data.model.name}</code>
@ -170,12 +183,12 @@ if (data.model.name === "@cf/runwayml/stable-diffusion-v1-5-inpainting") {
<a href={`/workers-ai/models/#${data.task_type}`}>{data.model.task.name}</a>
</p>
{showPlayground && <Playground name={data.model.name} />}
<CodeExamples name={data.model.name} lora={hasLora} />
{CodeExamples && <CodeExamples name={data.model.name} lora={hasLora} />}
{
data.task_type === "text-generation" && (
<TextGenerationPrompting lora={hasLora} />
)
}
<Responses name={data.model.name} />
{Responses && <Responses name={data.model.name} />}
<Schemas schemas={data.json_schema} />
</StarlightPage>
</StarlightPage>

View file

@ -2,16 +2,26 @@
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import { Image } from "astro:assets";
import CursorDark from "~/assets/images/workers/ai/cursor-dark.png"
import CursorLight from "~/assets/images/workers/ai/cursor-light.png"
import CursorDark from "~/assets/images/workers/ai/cursor-dark.png";
import CursorLight from "~/assets/images/workers/ai/cursor-light.png";
---
<StarlightPage frontmatter={{ title: "AI Assistant", tableOfContents: false }}>
<section id="assistant-header">
<div class="not-content">
<Image src={CursorDark} alt="Cursor illustration" width="300" class="light:sl-hidden"/>
<Image src={CursorLight} alt="Cursor illustration" width="300" class="dark:sl-hidden"/>
</div>
<div class="not-content">
<Image
src={CursorDark}
alt="Cursor illustration"
width="300"
class="light:sl-hidden"
/>
<Image
src={CursorLight}
alt="Cursor illustration"
width="300"
class="dark:sl-hidden"
/>
</div>
<div id="text">
<h1>
<div class="illustration"></div>
@ -37,7 +47,12 @@ import CursorLight from "~/assets/images/workers/ai/cursor-light.png"
<ul id="messages"></ul>
<form id="ai-form">
<input id="prompt" placeholder="How do Cloudflare Workers work?" minlength="8" maxlength="256" />
<input
id="prompt"
placeholder="How do Cloudflare Workers work?"
minlength="8"
maxlength="256"
/>
<button type="submit">→</button>
</form>
@ -268,112 +283,107 @@ import CursorLight from "~/assets/images/workers/ai/cursor-light.png"
</style>
<script is:inline>
window.addEventListener("DOMContentLoaded", () => {
const formEl = document.getElementById("ai-form");
const promptEl = document.getElementById("prompt");
const messagesEl = document.getElementById("messages");
window.addEventListener("DOMContentLoaded", () => {
const formEl = document.getElementById("ai-form");
const promptEl = document.getElementById("prompt");
const messagesEl = document.getElementById("messages");
const messages = {};
const sourceToUrl = (mdFilename) =>
`https://developers.cloudflare.com/${mdFilename
.replace(/_index\.md$/, "")
.replace(/\.md$/, "")}`;
const sourceToUrl = (mdFilename) =>
`https://developers.cloudflare.com/${mdFilename
.replace(/_index\.md$/, "")
.replace(/\.md$/, "")}`;
formEl.addEventListener("submit", (e) => {
e.preventDefault();
e.stopPropagation();
formEl.addEventListener("submit", (e) => {
e.preventDefault();
e.stopPropagation();
const prompt = promptEl.value;
const prompt = promptEl.value;
if (prompt.length < 8 || prompt.length > 256) return;
if (prompt.length < 8 || prompt.length > 256) return;
promptEl.value = "";
promptEl.value = "";
const messageEl = document.createElement("li");
messageEl.setAttribute("class", "prompt");
messageEl.innerText = prompt;
const messageEl = document.createElement("li");
messageEl.setAttribute("class", "prompt");
messageEl.innerText = prompt;
messagesEl.appendChild(messageEl);
messagesEl.appendChild(messageEl);
const url = new URL("https://bot-api.developers.cloudflare.com/stream");
url.searchParams.set("question", prompt);
const source = new EventSource(url);
const url = new URL("https://bot-api.developers.cloudflare.com/stream");
url.searchParams.set("question", prompt);
const source = new EventSource(url);
let completion = "";
let sources = [];
let completion = "";
let sources = [];
const responseEl = document.createElement("li");
responseEl.setAttribute("class", "response");
const responseEl = document.createElement("li");
responseEl.setAttribute("class", "response");
const responseTextEl = document.createElement("p");
responseEl.appendChild(responseTextEl);
const sourcesEl = document.createElement("ul");
sourcesEl.style.display = "none";
responseEl.appendChild(sourcesEl);
const responseTextEl = document.createElement("p");
responseEl.appendChild(responseTextEl);
const sourcesEl = document.createElement("ul");
sourcesEl.style.display = "none";
responseEl.appendChild(sourcesEl);
const loadingEl = document.createElement("div");
loadingEl.setAttribute("class", "loader");
loadingEl.innerHTML = '<div class="dot-flashing"></div>';
responseEl.appendChild(loadingEl);
const loadingEl = document.createElement("div");
loadingEl.setAttribute("class", "loader");
loadingEl.innerHTML = '<div class="dot-flashing"></div>';
responseEl.appendChild(loadingEl);
messagesEl.appendChild(responseEl);
messagesEl.appendChild(responseEl);
promptEl.setAttribute("disabled", true);
promptEl.setAttribute("disabled", true);
source.onerror = () => {
// Errors within an EventSource are indiscernible — this could occur due to rate limits or issues with a prompt.
// Let's fail gracefully and revert to default state.
source.onerror = (e) => {
// Errors within an EventSource are indiscernible — this could occur due to rate limits or issues with a prompt.
// Let's fail gracefully and revert to default state.
loadingEl.remove();
promptEl.removeAttribute("disabled");
loadingEl.remove();
promptEl.removeAttribute("disabled");
responseTextEl.innerText =
"I'm currently unable to answer this question. Please try rephrasing, or ask again at a later date.";
responseTextEl.innerText =
"I'm currently unable to answer this question. Please try rephrasing, or ask again at a later date.";
source.close();
};
source.close();
};
source.onmessage = ({ data }) => {
if (data === "[DONE]") {
source.close();
source.onmessage = ({ data }) => {
if (data === "[DONE]") {
source.close();
promptEl.removeAttribute("disabled");
promptEl.removeAttribute("disabled");
return;
}
return;
}
loadingEl.remove();
loadingEl.remove();
const {
choices: [{ text }],
} = JSON.parse(data);
completion += text;
const {
choices: [{ text }],
} = JSON.parse(data);
completion += text;
//const isCompleted = completion.indexOf("SOURCES:") > -1;
const isCompleted = completion.indexOf("SOURCES:") > -1;
const [answer, sourcesString] = completion.split("SOURCES:");
const [answer, sourcesString] = completion.split("SOURCES:");
responseTextEl.innerText = answer.trim();
responseTextEl.innerText = answer.trim();
if (sourcesString) {
sourcesEl.style.display = "block";
sources = sourcesString
.trim()
.split(", ")
.map((s) => sourceToUrl(s))
.filter((url) => url !== "https://developers.cloudflare.com/N/A");
}
if (sourcesString) {
sourcesEl.style.display = "block";
sources = sourcesString
.trim()
.split(", ")
.map((s) => sourceToUrl(s))
.filter((url) => url !== "https://developers.cloudflare.com/N/A");
}
sourcesEl.innerHTML =
"<li>These sources might provide additional context:</li>" +
sources
.map(
(url) =>
`<li class="source"><a href="${url}">${url}</a></li>`,
)
.join("");
};
});
});
sourcesEl.innerHTML =
"<li>These sources might provide additional context:</li>" +
sources
.map((url) => `<li class="source"><a href="${url}">${url}</a></li>`)
.join("");
};
});
});
</script>

View file

@ -1,31 +1,31 @@
import { getCollection } from "astro:content";
export async function GET() {
const entries = await getCollection("compatibility-dates");
entries.sort((a, b) => a.data.sort_date - b.data.sort_date)
const entries = await getCollection("compatibility-dates");
const flags = entries.flatMap((x) => {
delete x.data.sort_date;
if (!x.data.enable_flag) {
x.data.enable_flag = null
};
entries.sort((a, b) => a.data.sort_date.localeCompare(b.data.sort_date));
if (!x.data.enable_date) {
x.data.enable_date = null
}
const flags = entries.flatMap((x) => {
if (!x.data.enable_flag) {
x.data.enable_flag = null;
}
if (!x.data.disable_flag) {
x.data.disable_flag = null
}
if (!x.data.enable_date) {
x.data.enable_date = null;
}
return {
...x.data,
description: x.body.trim(),
experimental: x.data.experimental ?? false
}
});
if (!x.data.disable_flag) {
x.data.disable_flag = null;
}
return Response.json(flags);
}
// omit sort_date from output
const { sort_date, ...data } = x.data;
return {
...data,
description: x.body.trim(),
experimental: x.data.experimental ?? false,
};
});
return Response.json(flags);
}

View file

@ -73,6 +73,7 @@ export const baseSchema = z.object({
operation: z.string().array().optional(),
sidebar: z
.object({
order: z.number().optional(),
group: z
.object({
label: z

View file

@ -4,9 +4,9 @@ export type CompatibilityDatesSchema = z.infer<typeof compatibilityDatesSchema>;
export const compatibilityDatesSchema = z.object({
name: z.string(),
enable_date: z.string().optional(),
enable_flag: z.string(),
disable_flag: z.string().optional(),
enable_date: z.string().optional().nullable(),
enable_flag: z.string().nullable(),
disable_flag: z.string().optional().nullable(),
sort_date: z.string(),
experimental: z.boolean().optional(),
});

View file

@ -20,5 +20,6 @@ export const pagesBuildEnvironmentSchema = z
operating_system: z.string(),
architecture: z.string(),
}),
status: z.string().optional().nullable(),
})
.strict();

View file

@ -1,6 +1,7 @@
const links = document.querySelectorAll<HTMLAnchorElement>("a");
function $zarazLinkEvent(type: string, link: HTMLAnchorElement) {
// @ts-expect-error TODO: type zaraz
zaraz.track(type, { href: link.href, hostname: link.hostname });
}

View file

@ -2,21 +2,30 @@ import { z } from "astro:schema";
import { getCollection } from "astro:content";
import { type CollectionEntry } from "astro:content";
export async function getChangelogs(opts?: { filter?: Function, wranglerOnly?: boolean }) {
export async function getChangelogs(opts?: {
filter?: Parameters<typeof getCollection<"changelogs">>[1];
wranglerOnly?: boolean;
}) {
let changelogs;
if (opts?.wranglerOnly) {
changelogs = [await getWranglerChangelog()];
} else if (opts?.filter) {
changelogs = await getCollection("changelogs", opts.filter);
} else {
changelogs = await getCollection("changelogs", opts?.filter);
changelogs = await getCollection("changelogs");
}
if (!changelogs) {
throw new Error(`[getChangelogs] Unable to find any changelogs with ${JSON.stringify(opts)}`);
throw new Error(
`[getChangelogs] Unable to find any changelogs with ${JSON.stringify(opts)}`,
);
}
const products = [...new Set(changelogs.flatMap((x) => x.data.productName))];
const productAreas = [...new Set(changelogs.flatMap((x) => x.data.productArea))];
const productAreas = [
...new Set(changelogs.flatMap((x) => x.data.productArea)),
];
const mapped = changelogs.flatMap((product) => {
return product.data.entries.map((entry) => {
@ -64,7 +73,7 @@ export async function getWranglerChangelog(): Promise<
.array()
.parse(json);
releases = releases.filter(x => x.name.startsWith("wrangler@"))
releases = releases.filter((x) => x.name.startsWith("wrangler@"));
return {
// @ts-expect-error id is a union of on-disk YAML files but we're adding this one dynamically

View file

@ -6,5 +6,5 @@
"~/*": ["src/*"]
}
},
"exclude": ["dist"]
"exclude": ["dist", "functions"]
}