Skip to content

Commit ecbc834

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 85789e4 commit ecbc834

File tree

13 files changed

+220
-875
lines changed

13 files changed

+220
-875
lines changed

astro.config.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ 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";
@@ -27,6 +28,15 @@ import { devServerFileWatcher } from "./src/integrations/dev-server-file-watcher
2728
// import { isMoveReferenceEnabled } from "./src/utils/isMoveReferenceEnabled";
2829
// import { rehypeAddDebug } from "./src/plugins";
2930

31+
const i18nMatcherGenerator = await tsImport(
32+
"./integrations/i18n-matcher-generator/index.ts",
33+
import.meta.url,
34+
).then((m) => m.default);
35+
const vercelMiddlewareIntegration = await tsImport(
36+
"./integrations/vercel-middleware/index.ts",
37+
import.meta.url,
38+
).then((m) => m.default);
39+
3040
const ALGOLIA_APP_ID = ENV.ALGOLIA_APP_ID;
3141
const ALGOLIA_SEARCH_API_KEY = ENV.ALGOLIA_SEARCH_API_KEY;
3242
const ALGOLIA_INDEX_NAME = ENV.ALGOLIA_INDEX_NAME;
@@ -196,6 +206,11 @@ export default defineConfig({
196206
],
197207
},
198208
}),
209+
i18nMatcherGenerator({
210+
supportedLanguages: SUPPORTED_LANGUAGES,
211+
manualMatchers: ["/en", "/en/:path*"],
212+
}),
213+
vercelMiddlewareIntegration(),
199214
],
200215
adapter: process.env.VERCEL
201216
? 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)