Skip to content

Commit c1b287e

Browse files
committed
Convert all existing middlewares into Astro middlewares. Get rid of the old scripts. Convert matchers generation script into another Astro integration, to have better DX (it regenerates file automatically according to the Astro pipeline and doesn't require any extra vague scripts setup).
1 parent d966741 commit c1b287e

File tree

19 files changed

+287
-893
lines changed

19 files changed

+287
-893
lines changed

.github/actions/prepare-runner/action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ runs:
2626
2727
- name: Install dependencies
2828
shell: bash
29-
run: pnpm install --frozen-lockfile
29+
run: |
30+
./scripts/create_npmrc.sh
31+
pnpm install --frozen-lockfile

.github/workflows/check.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ jobs:
1515
steps:
1616
- uses: actions/checkout@v4
1717
- uses: ./.github/actions/prepare-runner
18+
env:
19+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1820
- run: pnpm check

.github/workflows/lint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ jobs:
1515
steps:
1616
- uses: actions/checkout@v4
1717
- uses: ./.github/actions/prepare-runner
18+
env:
19+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1820
- run: pnpm sync
1921
- run: pnpm lint

astro.config.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import tailwindcss from "@tailwindcss/vite";
66
import starlightOpenAPI from "starlight-openapi";
77
import starlightDocSearch from "@astrojs/starlight-docsearch";
88

9+
import { tsImport } from "tsx/esm/api";
910
import vercel from "@astrojs/vercel";
1011
import remarkMath from "remark-math";
1112
import rehypeKatex from "rehype-katex";
1213
import rehypeRaw from "rehype-raw";
1314
import sitemap from "@astrojs/sitemap";
1415
import partytown from "@astrojs/partytown";
1516
import node from "@astrojs/node";
17+
import vercelMiddlewareIntegration from "@aptos-foundation/astro-vercel-middleware";
1618
import react from "@astrojs/react";
1719
import starlightLlmsTxt from "starlight-llms-txt";
1820
import favicons from "astro-favicons";
@@ -28,6 +30,11 @@ import onDemandDirective from "./src/integrations/client-on-demand/register.js";
2830
// import { isMoveReferenceEnabled } from "./src/utils/isMoveReferenceEnabled";
2931
// import { rehypeAddDebug } from "./src/plugins";
3032

33+
const i18nMatcherGenerator = await tsImport(
34+
"./integrations/i18n-matcher-generator/index.ts",
35+
import.meta.url,
36+
).then((m) => m.default);
37+
3138
const ALGOLIA_APP_ID = ENV.ALGOLIA_APP_ID;
3239
const ALGOLIA_SEARCH_API_KEY = ENV.ALGOLIA_SEARCH_API_KEY;
3340
const ALGOLIA_INDEX_NAME = ENV.ALGOLIA_INDEX_NAME;
@@ -201,6 +208,11 @@ export default defineConfig({
201208
],
202209
},
203210
}),
211+
i18nMatcherGenerator({
212+
supportedLanguages: SUPPORTED_LANGUAGES,
213+
manualMatchers: ["/en", "/en/:path*"],
214+
}),
215+
vercelMiddlewareIntegration(),
204216
],
205217
adapter: process.env.VERCEL
206218
? vercel({
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { fileURLToPath } from "node:url";
2+
import path from "node:path";
3+
import fs from "node:fs/promises";
4+
import type { AstroConfig, AstroIntegration } from "astro";
5+
import { getDirectoriesList, getGeneratedFileTemplate, isDirectory, isPathExist } from "./utils";
6+
7+
interface SupportedLanguage {
8+
code: string;
9+
label: string;
10+
default?: boolean;
11+
}
12+
13+
/**
14+
* Manual routes for middleware configuration
15+
* Add any custom routes here that aren't automatically detected
16+
*
17+
* Examples: '/custom-page/:path*', '/api/:path*',
18+
*/
19+
type ManualRouteMatcher = string;
20+
21+
interface i18nMatcherGeneratorOptions {
22+
supportedLanguages: SupportedLanguage[];
23+
manualMatchers?: ManualRouteMatcher[];
24+
}
25+
26+
export default function i18nMatcherGenerator({
27+
supportedLanguages,
28+
manualMatchers,
29+
}: i18nMatcherGeneratorOptions) {
30+
let astroConfig: AstroConfig;
31+
32+
const integration: AstroIntegration = {
33+
name: "i18n-matcher-generator",
34+
hooks: {
35+
"astro:config:setup": ({ config }) => {
36+
astroConfig = config;
37+
},
38+
"astro:routes:resolved": async ({ logger }) => {
39+
const rootDir = fileURLToPath(astroConfig.root);
40+
41+
// Get non-English locale codes
42+
const NON_ENGLISH_LOCALES = supportedLanguages
43+
.filter((lang) => lang.code !== "en")
44+
.map((lang) => lang.code);
45+
46+
// Discover content paths from src/content/docs
47+
const contentDocsPath = path.join(rootDir, "src/content/docs");
48+
const contentDirs = await getDirectoriesList(contentDocsPath, NON_ENGLISH_LOCALES).catch(
49+
(error: unknown) => {
50+
console.warn(`Could not read directories from ${contentDocsPath}: ${String(error)}`);
51+
52+
return [];
53+
},
54+
);
55+
const contentPaths = contentDirs.map((dir) => `/${dir}/:path*`);
56+
// Discover paths from src/pages/[...lang]
57+
const langPagesPath = path.join(rootDir, "src/pages/[...lang]");
58+
let pagePaths: string[] = [];
59+
60+
if (await isDirectory(langPagesPath)) {
61+
try {
62+
const pageFiles = await fs.readdir(langPagesPath, { withFileTypes: true });
63+
64+
// Get .astro files
65+
const astroFiles = pageFiles
66+
.filter((file) => file.isFile() && file.name.endsWith(".astro"))
67+
.map((file) => `/${file.name.replace(".astro", "")}`);
68+
69+
// Get directories
70+
const pageDirectories = pageFiles
71+
.filter((dirent) => dirent.isDirectory())
72+
.map((dirent) => `/${dirent.name}/:path*`);
73+
74+
pagePaths = [...astroFiles, ...pageDirectories];
75+
} catch (error) {
76+
logger.warn(`Could not read files from ${langPagesPath}: ${String(error)}`);
77+
}
78+
}
79+
80+
// Check if API reference is enabled from environment variables
81+
const apiReferencePaths = [];
82+
try {
83+
const envPath = path.join(rootDir, ".env");
84+
if (await isPathExist(envPath)) {
85+
const envContent = await fs.readFile(envPath, "utf8");
86+
if (envContent.includes("ENABLE_API_REFERENCE=true")) {
87+
apiReferencePaths.push("/api-reference/:path*");
88+
}
89+
}
90+
} catch (error) {
91+
logger.warn(`Could not check for API reference configuration: ${String(error)}`);
92+
}
93+
94+
// Combine all content paths
95+
const ALL_CONTENT_PATHS = [
96+
"/",
97+
...contentPaths,
98+
...pagePaths,
99+
...apiReferencePaths,
100+
...(manualMatchers ?? []),
101+
];
102+
103+
// Generate language-specific paths
104+
const LANGUAGE_PATHS: string[] = [];
105+
NON_ENGLISH_LOCALES.forEach((code) => {
106+
// Add the base language path with exact matching to avoid matching _astro paths
107+
LANGUAGE_PATHS.push(`/${code}$`);
108+
109+
// Add localized versions of all content paths (except the root path)
110+
ALL_CONTENT_PATHS.forEach((contentPath) => {
111+
// Skip the root path as we already have /{code}
112+
if (contentPath !== "/") {
113+
LANGUAGE_PATHS.push(`/${code}${contentPath}`);
114+
}
115+
});
116+
});
117+
118+
// Combine all paths
119+
const ALL_PATHS = [...ALL_CONTENT_PATHS, ...LANGUAGE_PATHS];
120+
121+
// Write the file
122+
const outputPath = path.join(rootDir, "src/middlewares/matcher-routes-dynamic.js");
123+
await fs.writeFile(outputPath, getGeneratedFileTemplate(ALL_PATHS));
124+
logger.info("Middleware matcher file generated successfully!");
125+
},
126+
},
127+
};
128+
129+
return integration;
130+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import fs from "node:fs/promises";
2+
3+
/**
4+
* Function to get directories from a path, excluding language directories
5+
*/
6+
export async function getDirectoriesList(path: string, excludeDirs: string[] = []) {
7+
return fs
8+
.readdir(path, { withFileTypes: true })
9+
.then((dirents) =>
10+
dirents
11+
.filter((dirent) => dirent.isDirectory() && !excludeDirs.includes(dirent.name))
12+
.map((dirent) => dirent.name),
13+
);
14+
}
15+
16+
export async function isPathExist(path: string) {
17+
try {
18+
await fs.access(path);
19+
return true;
20+
} catch {
21+
return false;
22+
}
23+
}
24+
25+
export async function isDirectory(path: string) {
26+
const stat = await fs.stat(path);
27+
28+
return stat.isDirectory();
29+
}
30+
31+
export function getGeneratedFileTemplate(allPaths: string[]): string {
32+
return `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT DIRECTLY
33+
// Generated on ${new Date().toISOString()}
34+
35+
export const i18MatcherRegexp = new RegExp(\`^(${allPaths.map((p) => p.replace(/:\w+\*/g, "[^/]+")).join("|")})$\`);
36+
`;
37+
}

0 commit comments

Comments
 (0)