mirror of
https://github.com/withastro/astro.git
synced 2025-03-10 23:01:26 -05:00
refactor(core): move actions inside core (#13262)
* refactor(core): remove middleware from actions * chore: fix tests * chore: skip e2e test due to db issues * chore: import actions later during server life cycle * chore: rename to just use the word "actions" * chore: address review * Update packages/astro/src/actions/loadActions.ts Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> * chore: remove gotcha --------- Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> Co-authored-by: bholmesdev <51384119+bholmesdev@users.noreply.github.com> Co-authored-by: JacobNWolf <27933270+JacobNWolf@users.noreply.github.com> Co-authored-by: matthewp <361671+matthewp@users.noreply.github.com> Co-authored-by: florian-lefebvre <69633530+florian-lefebvre@users.noreply.github.com>
This commit is contained in:
parent
e8c26d5bdb
commit
0025df37af
28 changed files with 233 additions and 110 deletions
5
.changeset/twelve-carrots-raise.md
Normal file
5
.changeset/twelve-carrots-raise.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactor Astro Actions to not use a middleware. Doing so should avoid unexpected issues when using the Astro middleware at the edge.
|
|
@ -15,16 +15,16 @@ export const server = {
|
||||||
handler: async ({ postId }) => {
|
handler: async ({ postId }) => {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
|
||||||
const { likes } = await db
|
const result = await db
|
||||||
.update(Likes)
|
.update(Likes)
|
||||||
.set({
|
.set({
|
||||||
likes: sql`likes + 1`,
|
likes: sql`${Likes.likes} + 1`,
|
||||||
})
|
})
|
||||||
.where(eq(Likes.postId, postId))
|
.where(eq(Likes.postId, postId))
|
||||||
.returning()
|
.returning()
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
return likes;
|
return result?.likes;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { actions, isInputError } from 'astro:actions';
|
import { actions, isInputError } from 'astro:actions';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import {createLoggerFromFlags} from "../../../../../src/cli/flags.ts";
|
||||||
|
|
||||||
export function PostComment({
|
export function PostComment({
|
||||||
postId,
|
postId,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getEntry } from 'astro:content';
|
import { type CollectionEntry, getEntry, render } from 'astro:content';
|
||||||
import BlogPost from '../../layouts/BlogPost.astro';
|
import BlogPost from '../../layouts/BlogPost.astro';
|
||||||
import { Logout } from '../../components/Logout';
|
import { Logout } from '../../components/Logout';
|
||||||
import { db, eq, Comment, Likes } from 'astro:db';
|
import { db, eq, Comment, Likes } from 'astro:db';
|
||||||
|
@ -11,7 +11,7 @@ import { isInputError } from 'astro:actions';
|
||||||
type Props = CollectionEntry<'blog'>;
|
type Props = CollectionEntry<'blog'>;
|
||||||
|
|
||||||
const post = await getEntry('blog', Astro.params.slug)!;
|
const post = await getEntry('blog', Astro.params.slug)!;
|
||||||
const { Content } = await post.render();
|
const { Content } = await render(post);
|
||||||
|
|
||||||
if (Astro.url.searchParams.has('like')) {
|
if (Astro.url.searchParams.has('like')) {
|
||||||
await Astro.callAction(actions.blog.like.orThrow, { postId: post.id });
|
await Astro.callAction(actions.blog.like.orThrow, { postId: post.id });
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export const VIRTUAL_MODULE_ID = 'astro:actions';
|
export const VIRTUAL_MODULE_ID = 'astro:actions';
|
||||||
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
||||||
export const ACTIONS_TYPES_FILE = 'actions.d.ts';
|
export const ACTIONS_TYPES_FILE = 'actions.d.ts';
|
||||||
export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions';
|
export const ASTRO_ACTIONS_INTERNAL_MODULE_ID = 'astro-internal:actions';
|
||||||
export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions';
|
export const RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID = '\0' + ASTRO_ACTIONS_INTERNAL_MODULE_ID;
|
||||||
export const NOOP_ACTIONS = '\0noop-actions';
|
export const NOOP_ACTIONS = '\0noop-actions';
|
||||||
|
|
||||||
export const ACTION_QUERY_PARAMS = {
|
export const ACTION_QUERY_PARAMS = {
|
||||||
|
|
|
@ -17,18 +17,13 @@ export default function astroIntegrationActionsRouteHandler({
|
||||||
return {
|
return {
|
||||||
name: VIRTUAL_MODULE_ID,
|
name: VIRTUAL_MODULE_ID,
|
||||||
hooks: {
|
hooks: {
|
||||||
async 'astro:config:setup'(params) {
|
async 'astro:config:setup'() {
|
||||||
settings.injectedRoutes.push({
|
settings.injectedRoutes.push({
|
||||||
pattern: ACTION_RPC_ROUTE_PATTERN,
|
pattern: ACTION_RPC_ROUTE_PATTERN,
|
||||||
entrypoint: 'astro/actions/runtime/route.js',
|
entrypoint: 'astro/actions/runtime/route.js',
|
||||||
prerender: false,
|
prerender: false,
|
||||||
origin: 'internal',
|
origin: 'internal',
|
||||||
});
|
});
|
||||||
|
|
||||||
params.addMiddleware({
|
|
||||||
entrypoint: 'astro/actions/runtime/middleware.js',
|
|
||||||
order: 'post',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
'astro:config:done': async (params) => {
|
'astro:config:done': async (params) => {
|
||||||
if (params.buildOutput === 'static') {
|
if (params.buildOutput === 'static') {
|
||||||
|
|
20
packages/astro/src/actions/loadActions.ts
Normal file
20
packages/astro/src/actions/loadActions.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import type {ModuleLoader} from "../core/module-loader/index.js";
|
||||||
|
import {ASTRO_ACTIONS_INTERNAL_MODULE_ID} from "./consts.js";
|
||||||
|
import type {SSRActions} from "../core/app/types.js";
|
||||||
|
import {ActionsCantBeLoaded} from "../core/errors/errors-data.js";
|
||||||
|
import {AstroError} from "../core/errors/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration.
|
||||||
|
*
|
||||||
|
* If not middlewares were not set, the function returns an empty array.
|
||||||
|
*/
|
||||||
|
export async function loadActions(moduleLoader: ModuleLoader) {
|
||||||
|
try {
|
||||||
|
return (await moduleLoader.import(
|
||||||
|
ASTRO_ACTIONS_INTERNAL_MODULE_ID,
|
||||||
|
)) as SSRActions;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new AstroError(ActionsCantBeLoaded, {cause: error});
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,16 @@ import { shouldAppendForwardSlash } from '../core/build/util.js';
|
||||||
import type { AstroSettings } from '../types/astro.js';
|
import type { AstroSettings } from '../types/astro.js';
|
||||||
import {
|
import {
|
||||||
NOOP_ACTIONS,
|
NOOP_ACTIONS,
|
||||||
RESOLVED_VIRTUAL_INTERNAL_MODULE_ID,
|
RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID,
|
||||||
RESOLVED_VIRTUAL_MODULE_ID,
|
RESOLVED_VIRTUAL_MODULE_ID,
|
||||||
VIRTUAL_INTERNAL_MODULE_ID,
|
ASTRO_ACTIONS_INTERNAL_MODULE_ID,
|
||||||
VIRTUAL_MODULE_ID,
|
VIRTUAL_MODULE_ID,
|
||||||
} from './consts.js';
|
} from './consts.js';
|
||||||
import { isActionsFilePresent } from './utils.js';
|
import { isActionsFilePresent } from './utils.js';
|
||||||
|
import { getOutputDirectory } from '../prerender/utils.js';
|
||||||
|
import type { StaticBuildOptions } from '../core/build/types.js';
|
||||||
|
import type { BuildInternals } from '../core/build/internal.js';
|
||||||
|
import { addRollupInput } from '../core/build/add-rollup-input.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin is responsible to load the known file `actions/index.js` / `actions.js`
|
* This plugin is responsible to load the known file `actions/index.js` / `actions.js`
|
||||||
|
@ -24,7 +28,7 @@ export function vitePluginUserActions({ settings }: { settings: AstroSettings })
|
||||||
if (id === NOOP_ACTIONS) {
|
if (id === NOOP_ACTIONS) {
|
||||||
return NOOP_ACTIONS;
|
return NOOP_ACTIONS;
|
||||||
}
|
}
|
||||||
if (id === VIRTUAL_INTERNAL_MODULE_ID) {
|
if (id === ASTRO_ACTIONS_INTERNAL_MODULE_ID) {
|
||||||
const resolvedModule = await this.resolve(
|
const resolvedModule = await this.resolve(
|
||||||
`${decodeURI(new URL('actions', settings.config.srcDir).pathname)}`,
|
`${decodeURI(new URL('actions', settings.config.srcDir).pathname)}`,
|
||||||
);
|
);
|
||||||
|
@ -33,20 +37,50 @@ export function vitePluginUserActions({ settings }: { settings: AstroSettings })
|
||||||
return NOOP_ACTIONS;
|
return NOOP_ACTIONS;
|
||||||
}
|
}
|
||||||
resolvedActionsId = resolvedModule.id;
|
resolvedActionsId = resolvedModule.id;
|
||||||
return RESOLVED_VIRTUAL_INTERNAL_MODULE_ID;
|
return RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
load(id) {
|
load(id) {
|
||||||
if (id === NOOP_ACTIONS) {
|
if (id === NOOP_ACTIONS) {
|
||||||
return 'export const server = {}';
|
return 'export const server = {}';
|
||||||
} else if (id === RESOLVED_VIRTUAL_INTERNAL_MODULE_ID) {
|
} else if (id === RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID) {
|
||||||
return `export { server } from '${resolvedActionsId}';`;
|
return `export { server } from '${resolvedActionsId}';`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This plugin is used to retrieve the final entry point of the bundled actions.ts file
|
||||||
|
* @param opts
|
||||||
|
* @param internals
|
||||||
|
*/
|
||||||
|
export function vitePluginActionsBuild(
|
||||||
|
opts: StaticBuildOptions,
|
||||||
|
internals: BuildInternals,
|
||||||
|
): VitePlugin {
|
||||||
|
return {
|
||||||
|
name: '@astro/plugin-actions-build',
|
||||||
|
|
||||||
|
options(options) {
|
||||||
|
return addRollupInput(options, [ASTRO_ACTIONS_INTERNAL_MODULE_ID]);
|
||||||
|
},
|
||||||
|
|
||||||
|
writeBundle(_, bundle) {
|
||||||
|
for (const [chunkName, chunk] of Object.entries(bundle)) {
|
||||||
|
if (
|
||||||
|
chunk.type !== 'asset' &&
|
||||||
|
chunk.facadeModuleId === RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID
|
||||||
|
) {
|
||||||
|
const outputDirectory = getOutputDirectory(opts.settings);
|
||||||
|
internals.astroActionsEntryPoint = new URL(chunkName, outputDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function vitePluginActions({
|
export function vitePluginActions({
|
||||||
fs,
|
fs,
|
||||||
settings,
|
settings,
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { defineMiddleware } from '../../virtual-modules/middleware.js';
|
|
||||||
import { getActionContext } from './virtual/server.js';
|
|
||||||
|
|
||||||
export const onRequest = defineMiddleware(async (context, next) => {
|
|
||||||
if (context.isPrerendered) return next();
|
|
||||||
const { action, setActionResult, serializeActionResult } = getActionContext(context);
|
|
||||||
|
|
||||||
if (action?.calledFrom === 'form') {
|
|
||||||
const actionResult = await action.handler();
|
|
||||||
setActionResult(action.name, serializeActionResult(actionResult));
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { ZodType } from 'zod';
|
|
||||||
import { ActionNotFoundError } from '../../../core/errors/errors-data.js';
|
|
||||||
import { AstroError } from '../../../core/errors/errors.js';
|
|
||||||
import type { ActionAccept, ActionClient } from './server.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get server-side action based on the route path.
|
|
||||||
* Imports from the virtual module `astro:internal-actions`, which maps to
|
|
||||||
* the user's `src/actions/index.ts` file at build-time.
|
|
||||||
*/
|
|
||||||
export async function getAction(
|
|
||||||
path: string,
|
|
||||||
): Promise<ActionClient<unknown, ActionAccept, ZodType>> {
|
|
||||||
const pathKeys = path.split('.').map((key) => decodeURIComponent(key));
|
|
||||||
// @ts-expect-error virtual module
|
|
||||||
let { server: actionLookup } = await import('astro:internal-actions');
|
|
||||||
|
|
||||||
if (actionLookup == null || !(typeof actionLookup === 'object')) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Expected \`server\` export in actions file to be an object. Received ${typeof actionLookup}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of pathKeys) {
|
|
||||||
if (!(key in actionLookup)) {
|
|
||||||
throw new AstroError({
|
|
||||||
...ActionNotFoundError,
|
|
||||||
message: ActionNotFoundError.message(pathKeys.join('.')),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
actionLookup = actionLookup[key];
|
|
||||||
}
|
|
||||||
if (typeof actionLookup !== 'function') {
|
|
||||||
throw new TypeError(
|
|
||||||
`Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof actionLookup}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return actionLookup;
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
isActionAPIContext,
|
isActionAPIContext,
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
import type { Locals } from '../utils.js';
|
import type { Locals } from '../utils.js';
|
||||||
import { getAction } from './get-action.js';
|
|
||||||
import {
|
import {
|
||||||
ACTION_QUERY_PARAMS,
|
ACTION_QUERY_PARAMS,
|
||||||
ActionError,
|
ActionError,
|
||||||
|
@ -239,7 +238,7 @@ function unwrapBaseObjectSchema(schema: z.ZodType, unparsedInput: FormData) {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActionMiddlewareContext = {
|
export type AstroActionContext = {
|
||||||
/** Information about an incoming action request. */
|
/** Information about an incoming action request. */
|
||||||
action?: {
|
action?: {
|
||||||
/** Whether an action was called using an RPC function or by using an HTML form action. */
|
/** Whether an action was called using an RPC function or by using an HTML form action. */
|
||||||
|
@ -268,7 +267,7 @@ export type ActionMiddlewareContext = {
|
||||||
/**
|
/**
|
||||||
* Access information about Action requests from middleware.
|
* Access information about Action requests from middleware.
|
||||||
*/
|
*/
|
||||||
export function getActionContext(context: APIContext): ActionMiddlewareContext {
|
export function getActionContext(context: APIContext): AstroActionContext {
|
||||||
const callerInfo = getCallerInfo(context);
|
const callerInfo = getCallerInfo(context);
|
||||||
|
|
||||||
// Prevents action results from being handled on a rewrite.
|
// Prevents action results from being handled on a rewrite.
|
||||||
|
@ -276,7 +275,7 @@ export function getActionContext(context: APIContext): ActionMiddlewareContext {
|
||||||
// if the user's middleware has already handled the result.
|
// if the user's middleware has already handled the result.
|
||||||
const actionResultAlreadySet = Boolean((context.locals as Locals)._actionPayload);
|
const actionResultAlreadySet = Boolean((context.locals as Locals)._actionPayload);
|
||||||
|
|
||||||
let action: ActionMiddlewareContext['action'] = undefined;
|
let action: AstroActionContext['action'] = undefined;
|
||||||
|
|
||||||
if (callerInfo && context.request.method === 'POST' && !actionResultAlreadySet) {
|
if (callerInfo && context.request.method === 'POST' && !actionResultAlreadySet) {
|
||||||
action = {
|
action = {
|
||||||
|
@ -291,7 +290,7 @@ export function getActionContext(context: APIContext): ActionMiddlewareContext {
|
||||||
? removeTrailingForwardSlash(callerInfo.name)
|
? removeTrailingForwardSlash(callerInfo.name)
|
||||||
: callerInfo.name;
|
: callerInfo.name;
|
||||||
|
|
||||||
const baseAction = await getAction(callerInfoName);
|
const baseAction = await pipeline.getAction(callerInfoName);
|
||||||
let input;
|
let input;
|
||||||
try {
|
try {
|
||||||
input = await parseRequestBody(context.request);
|
input = await parseRequestBody(context.request);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { parse as devalueParse, stringify as devalueStringify } from 'devalue';
|
import { parse as devalueParse, stringify as devalueStringify } from 'devalue';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import { REDIRECT_STATUS_CODES } from '../../../core/constants.js';
|
import { REDIRECT_STATUS_CODES } from '../../../core/constants.js';
|
||||||
import { ActionsReturnedInvalidDataError } from '../../../core/errors/errors-data.js';
|
import {ActionCalledFromServerError, ActionsReturnedInvalidDataError} from '../../../core/errors/errors-data.js';
|
||||||
import { AstroError } from '../../../core/errors/errors.js';
|
import { AstroError } from '../../../core/errors/errors.js';
|
||||||
import { appendForwardSlash as _appendForwardSlash } from '../../../core/path.js';
|
import { appendForwardSlash as _appendForwardSlash } from '../../../core/path.js';
|
||||||
import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from '../../consts.js';
|
import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from '../../consts.js';
|
||||||
|
@ -309,3 +309,7 @@ const actionResultErrorStack = (function actionResultErrorStackFn() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
export function astroCalledServerError(): AstroError {
|
||||||
|
return new AstroError(ActionCalledFromServerError);
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type {
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../types/public/internal.js';
|
} from '../../types/public/internal.js';
|
||||||
import type { SinglePageBuiltModule } from '../build/types.js';
|
import type { SinglePageBuiltModule } from '../build/types.js';
|
||||||
|
import type { ActionAccept, ActionClient } from '../../actions/runtime/virtual/server.js';
|
||||||
|
import type { ZodType } from 'zod';
|
||||||
|
|
||||||
export type ComponentPath = string;
|
export type ComponentPath = string;
|
||||||
|
|
||||||
|
@ -75,6 +77,7 @@ export type SSRManifest = {
|
||||||
key: Promise<CryptoKey>;
|
key: Promise<CryptoKey>;
|
||||||
i18n: SSRManifestI18n | undefined;
|
i18n: SSRManifestI18n | undefined;
|
||||||
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
|
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
|
||||||
|
actions?: SSRActions;
|
||||||
checkOrigin: boolean;
|
checkOrigin: boolean;
|
||||||
sessionConfig?: ResolvedSessionConfig<any>;
|
sessionConfig?: ResolvedSessionConfig<any>;
|
||||||
cacheDir: string | URL;
|
cacheDir: string | URL;
|
||||||
|
@ -85,6 +88,10 @@ export type SSRManifest = {
|
||||||
buildServerDir: string | URL;
|
buildServerDir: string | URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SSRActions = {
|
||||||
|
server: Record<string, ActionClient<unknown, ActionAccept, ZodType>>;
|
||||||
|
};
|
||||||
|
|
||||||
export type SSRManifestI18n = {
|
export type SSRManifestI18n = {
|
||||||
fallback: Record<string, string> | undefined;
|
fallback: Record<string, string> | undefined;
|
||||||
fallbackType: 'redirect' | 'rewrite';
|
fallbackType: 'redirect' | 'rewrite';
|
||||||
|
|
|
@ -14,6 +14,11 @@ import { NOOP_MIDDLEWARE_FN } from './middleware/noop-middleware.js';
|
||||||
import { sequence } from './middleware/sequence.js';
|
import { sequence } from './middleware/sequence.js';
|
||||||
import { RouteCache } from './render/route-cache.js';
|
import { RouteCache } from './render/route-cache.js';
|
||||||
import { createDefaultRoutes } from './routing/default.js';
|
import { createDefaultRoutes } from './routing/default.js';
|
||||||
|
import type { SSRActions } from './app/types.js';
|
||||||
|
import type { ActionAccept, ActionClient } from '../actions/runtime/virtual/server.js';
|
||||||
|
import type { ZodType } from 'zod';
|
||||||
|
import { AstroError } from './errors/index.js';
|
||||||
|
import { ActionNotFoundError } from './errors/errors-data.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Pipeline` represents the static parts of rendering that do not change between requests.
|
* The `Pipeline` represents the static parts of rendering that do not change between requests.
|
||||||
|
@ -24,6 +29,7 @@ import { createDefaultRoutes } from './routing/default.js';
|
||||||
export abstract class Pipeline {
|
export abstract class Pipeline {
|
||||||
readonly internalMiddleware: MiddlewareHandler[];
|
readonly internalMiddleware: MiddlewareHandler[];
|
||||||
resolvedMiddleware: MiddlewareHandler | undefined = undefined;
|
resolvedMiddleware: MiddlewareHandler | undefined = undefined;
|
||||||
|
resolvedActions: SSRActions | undefined = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly logger: Logger,
|
readonly logger: Logger,
|
||||||
|
@ -58,6 +64,8 @@ export abstract class Pipeline {
|
||||||
* Used to find the route module
|
* Used to find the route module
|
||||||
*/
|
*/
|
||||||
readonly defaultRoutes = createDefaultRoutes(manifest),
|
readonly defaultRoutes = createDefaultRoutes(manifest),
|
||||||
|
|
||||||
|
readonly actions = manifest.actions,
|
||||||
) {
|
) {
|
||||||
this.internalMiddleware = [];
|
this.internalMiddleware = [];
|
||||||
// We do use our middleware only if the user isn't using the manual setup
|
// We do use our middleware only if the user isn't using the manual setup
|
||||||
|
@ -114,6 +122,47 @@ export abstract class Pipeline {
|
||||||
return this.resolvedMiddleware;
|
return this.resolvedMiddleware;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActions(actions: SSRActions) {
|
||||||
|
this.resolvedActions = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActions(): Promise<SSRActions> {
|
||||||
|
if (this.resolvedActions) {
|
||||||
|
return this.resolvedActions;
|
||||||
|
} else if (this.actions) {
|
||||||
|
return this.actions;
|
||||||
|
}
|
||||||
|
return { server: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAction(path: string): Promise<ActionClient<unknown, ActionAccept, ZodType>> {
|
||||||
|
const pathKeys = path.split('.').map((key) => decodeURIComponent(key));
|
||||||
|
let { server } = await this.getActions();
|
||||||
|
|
||||||
|
if (!server || !(typeof server === 'object')) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected \`server\` export in actions file to be an object. Received ${typeof server}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of pathKeys) {
|
||||||
|
if (!(key in server)) {
|
||||||
|
throw new AstroError({
|
||||||
|
...ActionNotFoundError,
|
||||||
|
message: ActionNotFoundError.message(pathKeys.join('.')),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// @ts-expect-error we are doing a recursion... it's ugly
|
||||||
|
server = server[key];
|
||||||
|
}
|
||||||
|
if (typeof server !== 'function') {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return server;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
|
|
@ -27,7 +27,7 @@ import type {
|
||||||
SSRError,
|
SSRError,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../types/public/internal.js';
|
} from '../../types/public/internal.js';
|
||||||
import type { SSRManifest, SSRManifestI18n } from '../app/types.js';
|
import type { SSRActions, SSRManifest, SSRManifestI18n } from '../app/types.js';
|
||||||
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
|
import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
|
||||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js';
|
import { NOOP_MIDDLEWARE_FN } from '../middleware/noop-middleware.js';
|
||||||
|
@ -63,11 +63,16 @@ export async function generatePages(options: StaticBuildOptions, internals: Buil
|
||||||
const middleware: MiddlewareHandler = internals.middlewareEntryPoint
|
const middleware: MiddlewareHandler = internals.middlewareEntryPoint
|
||||||
? await import(internals.middlewareEntryPoint.toString()).then((mod) => mod.onRequest)
|
? await import(internals.middlewareEntryPoint.toString()).then((mod) => mod.onRequest)
|
||||||
: NOOP_MIDDLEWARE_FN;
|
: NOOP_MIDDLEWARE_FN;
|
||||||
|
|
||||||
|
const actions: SSRActions = internals.astroActionsEntryPoint
|
||||||
|
? await import(internals.astroActionsEntryPoint.toString()).then((mod) => mod)
|
||||||
|
: { server: {} };
|
||||||
manifest = createBuildManifest(
|
manifest = createBuildManifest(
|
||||||
options.settings,
|
options.settings,
|
||||||
internals,
|
internals,
|
||||||
renderers.renderers as SSRLoadedRenderer[],
|
renderers.renderers as SSRLoadedRenderer[],
|
||||||
middleware,
|
middleware,
|
||||||
|
actions,
|
||||||
options.key,
|
options.key,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -451,8 +456,7 @@ function getUrlForPath(
|
||||||
removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;
|
removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;
|
||||||
buildPathname = joinPaths(base, buildPathRelative);
|
buildPathname = joinPaths(base, buildPathRelative);
|
||||||
}
|
}
|
||||||
const url = new URL(buildPathname, origin);
|
return new URL(buildPathname, origin);
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeneratePathOptions {
|
interface GeneratePathOptions {
|
||||||
|
@ -599,6 +603,7 @@ function createBuildManifest(
|
||||||
internals: BuildInternals,
|
internals: BuildInternals,
|
||||||
renderers: SSRLoadedRenderer[],
|
renderers: SSRLoadedRenderer[],
|
||||||
middleware: MiddlewareHandler,
|
middleware: MiddlewareHandler,
|
||||||
|
actions: SSRActions,
|
||||||
key: Promise<CryptoKey>,
|
key: Promise<CryptoKey>,
|
||||||
): SSRManifest {
|
): SSRManifest {
|
||||||
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
||||||
|
@ -641,6 +646,7 @@ function createBuildManifest(
|
||||||
onRequest: middleware,
|
onRequest: middleware,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
actions,
|
||||||
checkOrigin:
|
checkOrigin:
|
||||||
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
|
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -87,7 +87,8 @@ export interface BuildInternals {
|
||||||
manifestFileName?: string;
|
manifestFileName?: string;
|
||||||
entryPoints: Map<RouteData, URL>;
|
entryPoints: Map<RouteData, URL>;
|
||||||
componentMetadata: SSRResult['componentMetadata'];
|
componentMetadata: SSRResult['componentMetadata'];
|
||||||
middlewareEntryPoint?: URL;
|
middlewareEntryPoint: URL | undefined;
|
||||||
|
astroActionsEntryPoint: URL | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chunks in the bundle that are only used in prerendering that we can delete later
|
* Chunks in the bundle that are only used in prerendering that we can delete later
|
||||||
|
@ -118,6 +119,8 @@ export function createBuildInternals(): BuildInternals {
|
||||||
componentMetadata: new Map(),
|
componentMetadata: new Map(),
|
||||||
entryPoints: new Map(),
|
entryPoints: new Map(),
|
||||||
prerenderOnlyChunks: [],
|
prerenderOnlyChunks: [],
|
||||||
|
astroActionsEntryPoint: undefined,
|
||||||
|
middlewareEntryPoint: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { pluginPrerender } from './plugin-prerender.js';
|
||||||
import { pluginRenderers } from './plugin-renderers.js';
|
import { pluginRenderers } from './plugin-renderers.js';
|
||||||
import { pluginScripts } from './plugin-scripts.js';
|
import { pluginScripts } from './plugin-scripts.js';
|
||||||
import { pluginSSR } from './plugin-ssr.js';
|
import { pluginSSR } from './plugin-ssr.js';
|
||||||
|
import { pluginActions } from './plugin-actions.js';
|
||||||
|
|
||||||
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
||||||
register(pluginComponentEntry(internals));
|
register(pluginComponentEntry(internals));
|
||||||
|
@ -21,6 +22,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
|
||||||
register(pluginManifest(options, internals));
|
register(pluginManifest(options, internals));
|
||||||
register(pluginRenderers(options));
|
register(pluginRenderers(options));
|
||||||
register(pluginMiddleware(options, internals));
|
register(pluginMiddleware(options, internals));
|
||||||
|
register(pluginActions(options, internals));
|
||||||
register(pluginPages(options, internals));
|
register(pluginPages(options, internals));
|
||||||
register(pluginCSS(options, internals));
|
register(pluginCSS(options, internals));
|
||||||
register(astroHeadBuildPlugin(internals));
|
register(astroHeadBuildPlugin(internals));
|
||||||
|
|
20
packages/astro/src/core/build/plugins/plugin-actions.ts
Normal file
20
packages/astro/src/core/build/plugins/plugin-actions.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { vitePluginActionsBuild } from '../../../actions/plugins.js';
|
||||||
|
import type { StaticBuildOptions } from '../types.js';
|
||||||
|
import type { BuildInternals } from '../internal.js';
|
||||||
|
import type { AstroBuildPlugin } from '../plugin.js';
|
||||||
|
|
||||||
|
export function pluginActions(
|
||||||
|
opts: StaticBuildOptions,
|
||||||
|
internals: BuildInternals,
|
||||||
|
): AstroBuildPlugin {
|
||||||
|
return {
|
||||||
|
targets: ['server'],
|
||||||
|
hooks: {
|
||||||
|
'build:before': () => {
|
||||||
|
return {
|
||||||
|
vitePlugin: vitePluginActionsBuild(opts, internals),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ import { vitePluginMiddlewareBuild } from '../../middleware/vite-plugin.js';
|
||||||
import type { BuildInternals } from '../internal.js';
|
import type { BuildInternals } from '../internal.js';
|
||||||
import type { AstroBuildPlugin } from '../plugin.js';
|
import type { AstroBuildPlugin } from '../plugin.js';
|
||||||
import type { StaticBuildOptions } from '../types.js';
|
import type { StaticBuildOptions } from '../types.js';
|
||||||
export { MIDDLEWARE_MODULE_ID } from '../../middleware/vite-plugin.js';
|
|
||||||
|
|
||||||
export function pluginMiddleware(
|
export function pluginMiddleware(
|
||||||
opts: StaticBuildOptions,
|
opts: StaticBuildOptions,
|
||||||
|
|
|
@ -7,10 +7,11 @@ import type { BuildInternals } from '../internal.js';
|
||||||
import type { AstroBuildPlugin } from '../plugin.js';
|
import type { AstroBuildPlugin } from '../plugin.js';
|
||||||
import type { StaticBuildOptions } from '../types.js';
|
import type { StaticBuildOptions } from '../types.js';
|
||||||
import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
|
import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
|
||||||
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
|
|
||||||
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
|
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
|
||||||
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
|
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
|
||||||
import { getVirtualModulePageName } from './util.js';
|
import { getVirtualModulePageName } from './util.js';
|
||||||
|
import { ASTRO_ACTIONS_INTERNAL_MODULE_ID } from '../../../actions/consts.js';
|
||||||
|
import { MIDDLEWARE_MODULE_ID } from '../../middleware/vite-plugin.js';
|
||||||
|
|
||||||
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
|
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
|
||||||
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
||||||
|
@ -167,6 +168,7 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
|
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
|
||||||
|
`import * as actions from '${ASTRO_ACTIONS_INTERNAL_MODULE_ID}';`,
|
||||||
`import * as serverEntrypointModule from '${ADAPTER_VIRTUAL_MODULE_ID}';`,
|
`import * as serverEntrypointModule from '${ADAPTER_VIRTUAL_MODULE_ID}';`,
|
||||||
`import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
|
`import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
|
||||||
`import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`,
|
`import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`,
|
||||||
|
@ -178,6 +180,7 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
|
||||||
` pageMap,`,
|
` pageMap,`,
|
||||||
` serverIslandMap,`,
|
` serverIslandMap,`,
|
||||||
` renderers,`,
|
` renderers,`,
|
||||||
|
` actions,`,
|
||||||
` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${middlewareId}")`}`,
|
` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${middlewareId}")`}`,
|
||||||
`});`,
|
`});`,
|
||||||
`const _args = ${adapter.args ? JSON.stringify(adapter.args, null, 4) : 'undefined'};`,
|
`const _args = ${adapter.args ? JSON.stringify(adapter.args, null, 4) : 'undefined'};`,
|
||||||
|
|
|
@ -901,6 +901,19 @@ export const MiddlewareCantBeLoaded = {
|
||||||
message: 'An unknown error was thrown while loading your middleware.',
|
message: 'An unknown error was thrown while loading your middleware.',
|
||||||
} satisfies ErrorData;
|
} satisfies ErrorData;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @description
|
||||||
|
* Thrown in development mode when the actions file can't be loaded.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const ActionsCantBeLoaded = {
|
||||||
|
name: 'ActionsCantBeLoaded',
|
||||||
|
title: "Can't load the Astro actions.",
|
||||||
|
message: 'An unknown error was thrown while loading the Astro actions file.',
|
||||||
|
} satisfies ErrorData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @docs
|
* @docs
|
||||||
* @see
|
* @see
|
||||||
|
|
|
@ -32,6 +32,8 @@ import { type Pipeline, Slots, getParams, getProps } from './render/index.js';
|
||||||
import { isRoute404or500, isRouteExternalRedirect, isRouteServerIsland } from './routing/match.js';
|
import { isRoute404or500, isRouteExternalRedirect, isRouteServerIsland } from './routing/match.js';
|
||||||
import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rewrite.js';
|
import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rewrite.js';
|
||||||
import { AstroSession } from './session.js';
|
import { AstroSession } from './session.js';
|
||||||
|
import { getActionContext } from '../actions/runtime/virtual/server.js';
|
||||||
|
import type {SSRActions} from "./app/types.js";
|
||||||
|
|
||||||
export const apiContextRoutesSymbol = Symbol.for('context.routes');
|
export const apiContextRoutesSymbol = Symbol.for('context.routes');
|
||||||
|
|
||||||
|
@ -44,6 +46,7 @@ export class RenderContext {
|
||||||
readonly pipeline: Pipeline,
|
readonly pipeline: Pipeline,
|
||||||
public locals: App.Locals,
|
public locals: App.Locals,
|
||||||
readonly middleware: MiddlewareHandler,
|
readonly middleware: MiddlewareHandler,
|
||||||
|
readonly actions: SSRActions,
|
||||||
// It must be a DECODED pathname
|
// It must be a DECODED pathname
|
||||||
public pathname: string,
|
public pathname: string,
|
||||||
public request: Request,
|
public request: Request,
|
||||||
|
@ -80,16 +83,19 @@ export class RenderContext {
|
||||||
status = 200,
|
status = 200,
|
||||||
props,
|
props,
|
||||||
partial = undefined,
|
partial = undefined,
|
||||||
|
actions
|
||||||
}: Pick<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData' | 'clientAddress'> &
|
}: Pick<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData' | 'clientAddress'> &
|
||||||
Partial<
|
Partial<
|
||||||
Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props' | 'partial'>
|
Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props' | 'partial' | 'actions'>
|
||||||
>): Promise<RenderContext> {
|
>): Promise<RenderContext> {
|
||||||
const pipelineMiddleware = await pipeline.getMiddleware();
|
const pipelineMiddleware = await pipeline.getMiddleware();
|
||||||
|
const pipelineActions = actions ?? await pipeline.getActions();
|
||||||
setOriginPathname(request, pathname);
|
setOriginPathname(request, pathname);
|
||||||
return new RenderContext(
|
return new RenderContext(
|
||||||
pipeline,
|
pipeline,
|
||||||
locals,
|
locals,
|
||||||
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
|
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
|
||||||
|
pipelineActions,
|
||||||
pathname,
|
pathname,
|
||||||
request,
|
request,
|
||||||
routeData,
|
routeData,
|
||||||
|
@ -132,7 +138,8 @@ export class RenderContext {
|
||||||
serverLike,
|
serverLike,
|
||||||
base: manifest.base,
|
base: manifest.base,
|
||||||
});
|
});
|
||||||
const apiContext = this.createAPIContext(props);
|
const actionApiContext = this.createActionAPIContext();
|
||||||
|
const apiContext = this.createAPIContext(props, actionApiContext);
|
||||||
|
|
||||||
this.counter++;
|
this.counter++;
|
||||||
if (this.counter === 4) {
|
if (this.counter === 4) {
|
||||||
|
@ -192,6 +199,15 @@ export class RenderContext {
|
||||||
}
|
}
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
|
if (!ctx.isPrerendered) {
|
||||||
|
const { action, setActionResult, serializeActionResult } = getActionContext(ctx);
|
||||||
|
|
||||||
|
if (action?.calledFrom === 'form') {
|
||||||
|
const actionResult = await action.handler();
|
||||||
|
setActionResult(action.name, serializeActionResult(actionResult));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (this.routeData.type) {
|
switch (this.routeData.type) {
|
||||||
case 'endpoint': {
|
case 'endpoint': {
|
||||||
response = await renderEndpoint(
|
response = await renderEndpoint(
|
||||||
|
@ -205,7 +221,7 @@ export class RenderContext {
|
||||||
case 'redirect':
|
case 'redirect':
|
||||||
return renderRedirect(this);
|
return renderRedirect(this);
|
||||||
case 'page': {
|
case 'page': {
|
||||||
const result = await this.createResult(componentInstance!);
|
const result = await this.createResult(componentInstance!, actionApiContext);
|
||||||
try {
|
try {
|
||||||
response = await renderPage(
|
response = await renderPage(
|
||||||
result,
|
result,
|
||||||
|
@ -263,8 +279,7 @@ export class RenderContext {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
createAPIContext(props: APIContext['props']): APIContext {
|
createAPIContext(props: APIContext['props'], context: ActionAPIContext): APIContext {
|
||||||
const context = this.createActionAPIContext();
|
|
||||||
const redirect = (path: string, status = 302) =>
|
const redirect = (path: string, status = 302) =>
|
||||||
new Response(null, { status, headers: { Location: path } });
|
new Response(null, { status, headers: { Location: path } });
|
||||||
Reflect.set(context, apiContextRoutesSymbol, this.pipeline);
|
Reflect.set(context, apiContextRoutesSymbol, this.pipeline);
|
||||||
|
@ -365,7 +380,7 @@ export class RenderContext {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createResult(mod: ComponentInstance) {
|
async createResult(mod: ComponentInstance, ctx: ActionAPIContext): Promise<SSRResult> {
|
||||||
const { cookies, pathname, pipeline, routeData, status } = this;
|
const { cookies, pathname, pipeline, routeData, status } = this;
|
||||||
const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } =
|
const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } =
|
||||||
pipeline;
|
pipeline;
|
||||||
|
@ -403,7 +418,7 @@ export class RenderContext {
|
||||||
cookies,
|
cookies,
|
||||||
/** This function returns the `Astro` faux-global */
|
/** This function returns the `Astro` faux-global */
|
||||||
createAstro: (astroGlobal, props, slots) =>
|
createAstro: (astroGlobal, props, slots) =>
|
||||||
this.createAstro(result, astroGlobal, props, slots),
|
this.createAstro(result, astroGlobal, props, slots, ctx),
|
||||||
links,
|
links,
|
||||||
params: this.params,
|
params: this.params,
|
||||||
partial,
|
partial,
|
||||||
|
@ -448,6 +463,7 @@ export class RenderContext {
|
||||||
astroStaticPartial: AstroGlobalPartial,
|
astroStaticPartial: AstroGlobalPartial,
|
||||||
props: Record<string, any>,
|
props: Record<string, any>,
|
||||||
slotValues: Record<string, any> | null,
|
slotValues: Record<string, any> | null,
|
||||||
|
apiContext: ActionAPIContext,
|
||||||
): AstroGlobal {
|
): AstroGlobal {
|
||||||
let astroPagePartial;
|
let astroPagePartial;
|
||||||
// During rewriting, we must recompute the Astro global, because we need to purge the previous params/props/etc.
|
// During rewriting, we must recompute the Astro global, because we need to purge the previous params/props/etc.
|
||||||
|
@ -455,12 +471,14 @@ export class RenderContext {
|
||||||
astroPagePartial = this.#astroPagePartial = this.createAstroPagePartial(
|
astroPagePartial = this.#astroPagePartial = this.createAstroPagePartial(
|
||||||
result,
|
result,
|
||||||
astroStaticPartial,
|
astroStaticPartial,
|
||||||
|
apiContext,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Create page partial with static partial so they can be cached together.
|
// Create page partial with static partial so they can be cached together.
|
||||||
astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial(
|
astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial(
|
||||||
result,
|
result,
|
||||||
astroStaticPartial,
|
astroStaticPartial,
|
||||||
|
apiContext,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Create component-level partials. `Astro.self` is added by the compiler.
|
// Create component-level partials. `Astro.self` is added by the compiler.
|
||||||
|
@ -493,6 +511,7 @@ export class RenderContext {
|
||||||
createAstroPagePartial(
|
createAstroPagePartial(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
astroStaticPartial: AstroGlobalPartial,
|
astroStaticPartial: AstroGlobalPartial,
|
||||||
|
apiContext: ActionAPIContext,
|
||||||
): Omit<AstroGlobal, 'props' | 'self' | 'slots'> {
|
): Omit<AstroGlobal, 'props' | 'self' | 'slots'> {
|
||||||
const renderContext = this;
|
const renderContext = this;
|
||||||
const { cookies, locals, params, pipeline, url, session } = this;
|
const { cookies, locals, params, pipeline, url, session } = this;
|
||||||
|
@ -511,6 +530,8 @@ export class RenderContext {
|
||||||
return await this.#executeRewrite(reroutePayload);
|
return await this.#executeRewrite(reroutePayload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const callAction = createCallAction(apiContext);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generator: astroStaticPartial.generator,
|
generator: astroStaticPartial.generator,
|
||||||
glob: astroStaticPartial.glob,
|
glob: astroStaticPartial.glob,
|
||||||
|
@ -539,7 +560,7 @@ export class RenderContext {
|
||||||
site: pipeline.site,
|
site: pipeline.site,
|
||||||
getActionResult: createGetActionResult(locals),
|
getActionResult: createGetActionResult(locals),
|
||||||
get callAction() {
|
get callAction() {
|
||||||
return createCallAction(this);
|
return callAction;
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
get originPathname() {
|
get originPathname() {
|
||||||
|
|
|
@ -212,16 +212,4 @@ export class DevPipeline extends Pipeline {
|
||||||
setManifestData(manifestData: RoutesList) {
|
setManifestData(manifestData: RoutesList) {
|
||||||
this.routesList = manifestData;
|
this.routesList = manifestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteKnownRoute(route: string, sourceRoute: RouteData): ComponentInstance {
|
|
||||||
if (this.serverLike && sourceRoute.prerender) {
|
|
||||||
for (let def of this.defaultRoutes) {
|
|
||||||
if (route === def.route) {
|
|
||||||
return def.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Unknown route');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default function createVitePluginAstroServer({
|
||||||
}: AstroPluginOptions): vite.Plugin {
|
}: AstroPluginOptions): vite.Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'astro:server',
|
name: 'astro:server',
|
||||||
configureServer(viteServer) {
|
async configureServer(viteServer) {
|
||||||
const loader = createViteLoader(viteServer);
|
const loader = createViteLoader(viteServer);
|
||||||
const pipeline = DevPipeline.create(routesList, {
|
const pipeline = DevPipeline.create(routesList, {
|
||||||
loader,
|
loader,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type { ComponentInstance, RoutesList } from '../types/astro.js';
|
||||||
import type { RouteData } from '../types/public/internal.js';
|
import type { RouteData } from '../types/public/internal.js';
|
||||||
import type { DevPipeline } from './pipeline.js';
|
import type { DevPipeline } from './pipeline.js';
|
||||||
import { writeSSRResult, writeWebResponse } from './response.js';
|
import { writeSSRResult, writeWebResponse } from './response.js';
|
||||||
|
import {loadActions} from "../actions/loadActions.js";
|
||||||
|
|
||||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
|
@ -159,6 +160,8 @@ export async function handleRoute({
|
||||||
let renderContext: RenderContext;
|
let renderContext: RenderContext;
|
||||||
let mod: ComponentInstance | undefined = undefined;
|
let mod: ComponentInstance | undefined = undefined;
|
||||||
let route: RouteData;
|
let route: RouteData;
|
||||||
|
const actions = await loadActions(loader);
|
||||||
|
pipeline.setActions(actions);
|
||||||
const middleware = (await loadMiddleware(loader)).onRequest;
|
const middleware = (await loadMiddleware(loader)).onRequest;
|
||||||
// This is required for adapters to set locals in dev mode. They use a dev server middleware to inject locals to the `http.IncomingRequest` object.
|
// This is required for adapters to set locals in dev mode. They use a dev server middleware to inject locals to the `http.IncomingRequest` object.
|
||||||
const locals = Reflect.get(incomingRequest, clientLocalsSymbol);
|
const locals = Reflect.get(incomingRequest, clientLocalsSymbol);
|
||||||
|
@ -192,6 +195,7 @@ export async function handleRoute({
|
||||||
request,
|
request,
|
||||||
routeData: route,
|
routeData: route,
|
||||||
clientAddress: incomingRequest.socket.remoteAddress,
|
clientAddress: incomingRequest.socket.remoteAddress,
|
||||||
|
actions
|
||||||
});
|
});
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
|
@ -4,8 +4,10 @@ import {
|
||||||
appendForwardSlash,
|
appendForwardSlash,
|
||||||
deserializeActionResult,
|
deserializeActionResult,
|
||||||
getActionQueryString,
|
getActionQueryString,
|
||||||
|
astroCalledServerError,
|
||||||
} from 'astro:actions';
|
} from 'astro:actions';
|
||||||
|
|
||||||
|
const apiContextRoutesSymbol = Symbol.for('context.routes');
|
||||||
const ENCODED_DOT = '%2E';
|
const ENCODED_DOT = '%2E';
|
||||||
|
|
||||||
function toActionProxy(actionCallback = {}, aggregatedPath = '') {
|
function toActionProxy(actionCallback = {}, aggregatedPath = '') {
|
||||||
|
@ -73,11 +75,13 @@ export function getActionPath(action) {
|
||||||
*/
|
*/
|
||||||
async function handleAction(param, path, context) {
|
async function handleAction(param, path, context) {
|
||||||
// When running server-side, import the action and call it.
|
// When running server-side, import the action and call it.
|
||||||
if (import.meta.env.SSR) {
|
if (import.meta.env.SSR && context) {
|
||||||
const { getAction } = await import('astro/actions/runtime/virtual/get-action.js');
|
const pipeline = Reflect.get(context, apiContextRoutesSymbol);
|
||||||
const action = await getAction(path);
|
if (!pipeline) {
|
||||||
|
throw astroCalledServerError();
|
||||||
|
}
|
||||||
|
const action = await pipeline.getAction(path);
|
||||||
if (!action) throw new Error(`Action not found: ${path}`);
|
if (!action) throw new Error(`Action not found: ${path}`);
|
||||||
|
|
||||||
return action.bind(context)(param);
|
return action.bind(context)(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,8 +60,8 @@ describe('Astro Actions', () => {
|
||||||
|
|
||||||
assert.equal(res.ok, true);
|
assert.equal(res.ok, true);
|
||||||
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
||||||
|
|
||||||
const data = devalue.parse(await res.text());
|
const data = devalue.parse(await res.text());
|
||||||
|
|
||||||
assert.equal(data.channel, 'bholmesdev');
|
assert.equal(data.channel, 'bholmesdev');
|
||||||
assert.equal(data.subscribeButtonState, 'smashed');
|
assert.equal(data.subscribeButtonState, 'smashed');
|
||||||
});
|
});
|
||||||
|
@ -578,7 +578,6 @@ it('Should support trailing slash', async () => {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(res.ok, true);
|
assert.equal(res.ok, true);
|
||||||
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
import { actions } from "astro:actions";
|
import { actions } from "astro:actions";
|
||||||
|
|
||||||
// this is invalid, it should fail
|
// this is invalid, it should fail
|
||||||
const result = await actions.subscribe({ channel: "hey" });
|
const result = await actions.subscribe({ channel: "hey" });
|
||||||
---
|
---
|
||||||
|
|
Loading…
Add table
Reference in a new issue