summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/astro/src/@types/astro.ts8
-rw-r--r--packages/astro/src/core/build/index.ts4
-rw-r--r--packages/astro/src/core/config.ts2
-rw-r--r--packages/astro/src/core/routing/manifest/create.ts77
-rw-r--r--packages/astro/src/core/routing/manifest/serialization.ts1
-rw-r--r--packages/astro/src/integrations/index.ts3
6 files changed, 78 insertions, 17 deletions
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 7dd341d6f..bc4f05a89 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -710,6 +710,11 @@ export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' |
* Resolved Astro Config
* Config with user settings along with all defaults filled in.
*/
+
+export interface InjectedRoute {
+ pattern: string,
+ entryPoint: string
+}
export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// Public:
// This is a more detailed type than zod validation gives us.
@@ -721,6 +726,7 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
// that is different from the user-exposed configuration.
// TODO: Create an AstroConfig class to manage this, long-term.
_ctx: {
+ injectedRoutes: InjectedRoute[],
adapter: AstroAdapter | undefined;
renderers: AstroRenderer[];
scripts: { stage: InjectedScriptStage; content: string }[];
@@ -929,6 +935,7 @@ export interface AstroIntegration {
updateConfig: (newConfig: Record<string, any>) => void;
addRenderer: (renderer: AstroRenderer) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
+ injectRoute: (injectRoute: InjectedRoute) => void;
// TODO: Add support for `injectElement()` for full HTML element injection, not just scripts.
// This may require some refactoring of `scripts`, `styles`, and `links` into something
// more generalized. Consider the SSR use-case as well.
@@ -966,6 +973,7 @@ export interface RoutePart {
}
export interface RouteData {
+ route: string,
component: string;
generate: (data?: any) => string;
params: string[];
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index 357255f0c..4f1e1c643 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -55,7 +55,7 @@ class AstroBuilder {
this.origin = config.site
? new URL(config.site).origin
: `http://localhost:${config.server.port}`;
- this.manifest = createRouteManifest({ config }, this.logging);
+ this.manifest = {routes: []};
this.timer = {};
}
@@ -66,6 +66,8 @@ class AstroBuilder {
this.timer.init = performance.now();
this.timer.viteStart = performance.now();
this.config = await runHookConfigSetup({ config: this.config, command: 'build' });
+ this.manifest = createRouteManifest({ config: this.config }, this.logging);
+
const viteConfig = await createVite(
{
mode: this.mode,
diff --git a/packages/astro/src/core/config.ts b/packages/astro/src/core/config.ts
index 669e0bcd2..5ef15a1af 100644
--- a/packages/astro/src/core/config.ts
+++ b/packages/astro/src/core/config.ts
@@ -338,7 +338,7 @@ export async function validateConfig(
// First-Pass Validation
const result = {
...(await AstroConfigRelativeSchema.parseAsync(userConfig)),
- _ctx: { scripts: [], renderers: [], adapter: undefined },
+ _ctx: { scripts: [], renderers: [], injectedRoutes: [], adapter: undefined },
};
// Final-Pass Validation (perform checks that require the full config object)
if (
diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts
index 4958e9abd..b68b69037 100644
--- a/packages/astro/src/core/routing/manifest/create.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -4,10 +4,12 @@ import type { LogOptions } from '../../logger/core';
import fs from 'fs';
import path from 'path';
import slash from 'slash';
+import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { warn } from '../../logger/core.js';
import { resolvePages } from '../../util.js';
import { getRouteGenerator } from './generator.js';
+const require = createRequire(import.meta.url);
interface Item {
basename: string;
@@ -93,6 +95,23 @@ function isSpread(str: string) {
return spreadPattern.test(str);
}
+function validateSegment(segment: string, file = '') {
+ if(!file) file = segment;
+
+ if (/^\$/.test(segment)) {
+ throw new Error(`Invalid route ${file} \u2014 Astro's Collections API has been replaced by dynamic route params.`);
+ }
+ if (/\]\[/.test(segment)) {
+ throw new Error(`Invalid route ${file} \u2014 parameters must be separated`);
+ }
+ if (countOccurrences("[", segment) !== countOccurrences("]", segment)) {
+ throw new Error(`Invalid route ${file} \u2014 brackets are unbalanced`);
+ }
+ if (/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) {
+ throw new Error(`Invalid route ${file} \u2014 rest parameter must be a standalone segment`);
+ }
+}
+
function comparator(a: Item, b: Item) {
if (a.isIndex !== b.isIndex) {
if (a.isIndex) return isSpread(a.file) ? 1 : -1;
@@ -168,20 +187,7 @@ export function createRouteManifest(
return;
}
const segment = isDir ? basename : name;
- if (/^\$/.test(segment)) {
- throw new Error(
- `Invalid route ${file} — Astro's Collections API has been replaced by dynamic route params.`
- );
- }
- if (/\]\[/.test(segment)) {
- throw new Error(`Invalid route ${file} — parameters must be separated`);
- }
- if (countOccurrences('[', segment) !== countOccurrences(']', segment)) {
- throw new Error(`Invalid route ${file} — brackets are unbalanced`);
- }
- if (/.+\[\.\.\.[^\]]+\]/.test(segment) || /\[\.\.\.[^\]]+\].+/.test(segment)) {
- throw new Error(`Invalid route ${file} — rest parameter must be a standalone segment`);
- }
+ validateSegment(segment, file);
const parts = getParts(segment, file);
const isIndex = isDir ? false : basename.startsWith('index.');
@@ -247,8 +253,10 @@ export function createRouteManifest(
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
? `/${segments.map((segment) => segment[0].content).join('/')}`
: null;
+ const route = `/${segments.map(([{dynamic, content}]) => dynamic ? `[${content}]` : content).join('/')}`.toLowerCase();
routes.push({
+ route,
type: item.isPage ? 'page' : 'endpoint',
pattern,
segments,
@@ -265,12 +273,51 @@ export function createRouteManifest(
if (fs.existsSync(pages)) {
walk(fileURLToPath(pages), [], []);
- } else {
+ } else if (config?._ctx?.injectedRoutes?.length === 0) {
const pagesDirRootRelative = pages.href.slice(config.root.href.length);
warn(logging, 'astro', `Missing pages directory: ${pagesDirRootRelative}`);
}
+ config?._ctx?.injectedRoutes?.forEach(({pattern: name, entryPoint}) => {
+ const resolved = require.resolve(entryPoint, { paths: [cwd || fileURLToPath(config.root)] });
+ const component = slash(path.relative(cwd || fileURLToPath(config.root), resolved));
+
+ const isDynamic = (str: string) => str?.[0] === '[';
+ const normalize = (str: string) => str?.substring(1, str?.length - 1);
+
+ const segments = name.split(path.sep)
+ .filter(Boolean)
+ .map((s: string) => {
+ validateSegment(s);
+
+ const dynamic = isDynamic(s);
+ const content = dynamic ? normalize(s) : s;
+ return [{
+ content,
+ dynamic,
+ spread: isSpread(s)
+ }]
+ });
+
+ const type = resolved.endsWith('.astro') ? 'page' : 'endpoint';
+ const isPage = type === 'page';
+ const trailingSlash = isPage ? config.trailingSlash : "never";
+
+ const pattern = getPattern(segments, trailingSlash);
+ const generate = getRouteGenerator(segments, trailingSlash);
+ const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join("/")}` : null;
+ const params = segments.flat().filter((p) => p.dynamic).map((p) => p.content);
+ const route = `/${segments.map(([{dynamic, content}]) => dynamic ? `[${content}]` : content).join('/')}`.toLowerCase();
+
+ const collision = routes.find(({route: r}) => r === route);
+ if(collision) {
+ throw new Error(`An integration attempted to inject a route that is already used in your project: "${route}" at "${component}". \nThis route collides with: "${collision.component}".`);
+ }
+
+ routes.push({type, route, pattern, segments, params, component, generate, pathname: pathname || void 0})
+ });
+
return {
routes,
};
diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts
index 5db04b49d..866c0d7bb 100644
--- a/packages/astro/src/core/routing/manifest/serialization.ts
+++ b/packages/astro/src/core/routing/manifest/serialization.ts
@@ -16,6 +16,7 @@ export function serializeRouteData(
export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteData {
return {
+ route: rawRouteData.route,
type: rawRouteData.type,
pattern: new RegExp(rawRouteData.pattern),
params: rawRouteData.params,
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index c119eb133..a1ec05f6a 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -46,6 +46,9 @@ export async function runHookConfigSetup({
updateConfig: (newConfig) => {
updatedConfig = mergeConfig(updatedConfig, newConfig) as AstroConfig;
},
+ injectRoute: (injectRoute) => {
+ updatedConfig._ctx.injectedRoutes.push(injectRoute);
+ },
});
}
}