Skip to content

Commit

Permalink
feat: rewriting (experimental) (#10867)
Browse files Browse the repository at this point in the history
* feat: implement reroute in dev (#10818)

* chore: implement reroute in dev

* chore: revert naming change

* chore: conditionally create the new request

* chore: handle error

* remove only

* remove only

* chore: add tests and remove logs

* chore: fix regression

* chore: fix regression route matching

* chore: remove unwanted test

* feat: reroute in SSG (#10843)

* feat: rerouting in ssg

* linting

* feat: reroute for SSR (#10845)

* feat: implement reroute in dev (#10818)

* chore: implement reroute in dev

* chore: revert naming change

* chore: conditionally create the new request

* chore: handle error

* remove only

* remove only

* chore: add tests and remove logs

* chore: fix regression

* chore: fix regression route matching

* chore: remove unwanted test

* feat: reroute in SSG (#10843)

* feat: rerouting in ssg

* linting

* feat: rerouting in ssg

* linting

* feat: reroute for SSR

* fix rebase

* fix merge issue

* feat: rerouting in the middleware (#10853)

* feat: implement reroute in dev (#10818)

* chore: implement reroute in dev

* chore: revert naming change

* chore: conditionally create the new request

* chore: handle error

* remove only

* remove only

* chore: add tests and remove logs

* chore: fix regression

* chore: fix regression route matching

* chore: remove unwanted test

* feat: reroute in SSG (#10843)

* feat: rerouting in ssg

* linting

* feat: rerouting in ssg

* linting

* feat: reroute for SSR

* fix rebase

* fix merge issue

* feat: implement the `next(payload)` feature for rerouting

* chore: revert code

* chore: fix code

* Apply suggestions from code review

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>

---------

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>

* feat: rerouting

* chore: rename to `rewrite`

* chore: better error message

* chore: update the chageset

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* chore: update docs based on feedback

* lock file

* Apply suggestions from code review

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by:  Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Ben Holmes <hey@bholmes.dev>

* feedback

* rename

* add tests for 404

* revert change

* fix regression

* Update .changeset/pink-ligers-share.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Ben Holmes <hey@bholmes.dev>
  • Loading branch information
5 people committed May 8, 2024
1 parent 7bbd664 commit 47877a7
Show file tree
Hide file tree
Showing 41 changed files with 1,192 additions and 222 deletions.
49 changes: 49 additions & 0 deletions .changeset/pink-ligers-share.md
@@ -0,0 +1,49 @@
---
"astro": minor
---

Adds experimental rewriting in Astro with a new `rewrite()` function and the middleware `next()` function.

The feature is available via an experimental flag in `astro.config.mjs`:

```js
export default defineConfig({
experimental: {
rewriting: true
}
})
```

When enabled, you can use `rewrite()` to **render** another page without changing the URL of the browser in Astro pages and endpoints.

```astro
---
// src/pages/dashboard.astro
if (!Astro.props.allowed) {
return Astro.rewrite("/")
}
---
```

```js
// src/pages/api.js
export function GET(ctx) {
if (!ctx.locals.allowed) {
return ctx.rewrite("/")
}
}
```

The middleware `next()` function now accepts a parameter with the same type as the `rewrite()` function. For example, with `next("/")`, you can call the next middleware function with a new `Request`.

```js
// src/middleware.js
export function onRequest(ctx, next) {
if (!ctx.cookies.get("allowed")) {
return next("/") // new signature
}
return next();
}
```

> **NOTE**: please [read the RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md) to understand the current expectations of the new APIs.
104 changes: 102 additions & 2 deletions packages/astro/src/@types/astro.ts
Expand Up @@ -251,6 +251,19 @@ export interface AstroGlobal<
* [Astro reference](https://docs.astro.build/en/guides/server-side-rendering/)
*/
redirect: AstroSharedContext['redirect'];
/**
* It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
* by the rewritten URL passed as argument.
*
* ## Example
*
* ```js
* if (pageIsNotEnabled) {
* return Astro.rewrite('/fallback-page')
* }
* ```
*/
rewrite: AstroSharedContext['rewrite'];
/**
* The <Astro.self /> element allows a component to reference itself recursively.
*
Expand Down Expand Up @@ -1642,7 +1655,7 @@ export interface AstroUserConfig {
domains?: Record<string, string>;
};

/** ⚠️ WARNING: SUBJECT TO CHANGE */
/** ! WARNING: SUBJECT TO CHANGE */
db?: Config.Database;

/**
Expand Down Expand Up @@ -1923,6 +1936,62 @@ export interface AstroUserConfig {
origin?: boolean;
};
};

/**
* @docs
* @name experimental.rewriting
* @type {boolean}
* @default `false`
* @version 4.8.0
* @description
*
* Enables a routing feature for rewriting requests in Astro pages, endpoints and Astro middleware, giving you programmatic control over your routes.
*
* ```js
* {
* experimental: {
* rewriting: true,
* },
* }
* ```
*
* Use `Astro.rewrite` in your `.astro` files to reroute to a different page:
*
* ```astro "rewrite"
* ---
* // src/pages/dashboard.astro
* if (!Astro.props.allowed) {
* return Astro.rewrite("/")
* }
* ---
* ```
*
* Use `context.rewrite` in your endpoint files to reroute to a different page:
*
* ```js
* // src/pages/api.js
* export function GET(ctx) {
* if (!ctx.locals.allowed) {
* return ctx.rewrite("/")
* }
* }
* ```
*
* Use `next("/")` in your middleware file to reroute to a different page, and then call the next middleware function:
*
* ```js
* // src/middleware.js
* export function onRequest(ctx, next) {
* if (!ctx.cookies.get("allowed")) {
* return next("/") // new signature
* }
* return next();
* }
* ```
*
* For a complete overview, and to give feedback on this experimental API, see the [Rerouting RFC](https://github.com/withastro/roadmap/blob/feat/reroute/proposals/0047-rerouting.md).
*/
rewriting: boolean;
};
}

Expand Down Expand Up @@ -2492,6 +2561,20 @@ interface AstroSharedContext<
*/
redirect(path: string, status?: ValidRedirectStatus): Response;

/**
* It rewrites to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
* by the rerouted URL passed as argument.
*
* ## Example
*
* ```js
* if (pageIsNotEnabled) {
* return Astro.rewrite('/fallback-page')
* }
* ```
*/
rewrite(rewritePayload: RewritePayload): Promise<Response>;

/**
* Object accessed via Astro middleware
*/
Expand Down Expand Up @@ -2606,6 +2689,21 @@ export interface APIContext<
*/
redirect: AstroSharedContext['redirect'];

/**
* It reroutes to another page. As opposed to redirects, the URL won't change, and Astro will render the HTML emitted
* by the rerouted URL passed as argument.
*
* ## Example
*
* ```ts
* // src/pages/secret.ts
* export function GET(ctx) {
* return ctx.rewrite(new URL("../"), ctx.url);
* }
* ```
*/
rewrite: AstroSharedContext['rewrite'];

/**
* An object that middlewares can use to store extra information related to the request.
*
Expand Down Expand Up @@ -2800,7 +2898,9 @@ export interface AstroIntegration {
};
}

export type MiddlewareNext = () => Promise<Response>;
export type RewritePayload = string | URL | Request;

export type MiddlewareNext = (rewritePayload?: RewritePayload) => Promise<Response>;
export type MiddlewareHandler = (
context: APIContext,
next: MiddlewareNext
Expand Down
52 changes: 7 additions & 45 deletions packages/astro/src/core/app/index.ts
@@ -1,13 +1,6 @@
import type {
ComponentInstance,
ManifestData,
RouteData,
SSRManifest,
} from '../../@types/astro.js';
import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
import { normalizeTheLocale } from '../../i18n/index.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import {
DEFAULT_404_COMPONENT,
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
clientAddressSymbol,
Expand All @@ -26,7 +19,6 @@ import {
prependForwardSlash,
removeTrailingForwardSlash,
} from '../path.js';
import { RedirectSinglePageBuiltModule } from '../redirects/index.js';
import { RenderContext } from '../render-context.js';
import { createAssetLink } from '../render/ssr-element.js';
import { ensure404Route } from '../routing/astro-designed-error-pages.js';
Expand Down Expand Up @@ -96,7 +88,7 @@ export class App {
routes: manifest.routes.map((route) => route.routeData),
});
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
this.#pipeline = this.#createPipeline(streaming);
this.#pipeline = this.#createPipeline(this.#manifestData, streaming);
this.#adapterLogger = new AstroIntegrationLogger(
this.#logger.options,
this.#manifest.adapterName
Expand All @@ -110,18 +102,19 @@ export class App {
/**
* Creates a pipeline by reading the stored manifest
*
* @param manifestData
* @param streaming
* @private
*/
#createPipeline(streaming = false) {
#createPipeline(manifestData: ManifestData, streaming = false) {
if (this.#manifest.checkOrigin) {
this.#manifest.middleware = sequence(
createOriginCheckMiddleware(),
this.#manifest.middleware
);
}

return AppPipeline.create({
return AppPipeline.create(manifestData, {
logger: this.#logger,
manifest: this.#manifest,
mode: 'production',
Expand Down Expand Up @@ -309,7 +302,7 @@ export class App {
}
const pathname = this.#getPathnameFromRequest(request);
const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
const mod = await this.#getModuleForRoute(routeData);
const mod = await this.#pipeline.getModuleForRoute(routeData);

let response;
try {
Expand Down Expand Up @@ -405,7 +398,7 @@ export class App {

return this.#mergeResponses(response, originalResponse, override);
}
const mod = await this.#getModuleForRoute(errorRouteData);
const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
try {
const renderContext = RenderContext.create({
locals,
Expand Down Expand Up @@ -493,35 +486,4 @@ export class App {
if (route.endsWith('/500')) return 500;
return 200;
}

async #getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
if (route.component === DEFAULT_404_COMPONENT) {
return {
page: async () =>
({ default: () => new Response(null, { status: 404 }) }) as ComponentInstance,
renderers: [],
};
}
if (route.type === 'redirect') {
return RedirectSinglePageBuiltModule;
} else {
if (this.#manifest.pageMap) {
const importComponentInstance = this.#manifest.pageMap.get(route.component);
if (!importComponentInstance) {
throw new Error(
`Unexpectedly unable to find a component instance for route ${route.route}`
);
}
const pageModule = await importComponentInstance();
return pageModule;
} else if (this.#manifest.pageModule) {
const importComponentInstance = this.#manifest.pageModule;
return importComponentInstance;
} else {
throw new Error(
"Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
);
}
}
}
}

0 comments on commit 47877a7

Please sign in to comment.