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 }) => {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
const { likes } = await db
|
||||
const result = await db
|
||||
.update(Likes)
|
||||
.set({
|
||||
likes: sql`likes + 1`,
|
||||
likes: sql`${Likes.likes} + 1`,
|
||||
})
|
||||
.where(eq(Likes.postId, postId))
|
||||
.returning()
|
||||
.get();
|
||||
|
||||
return likes;
|
||||
return result?.likes;
|
||||
},
|
||||
}),
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { actions, isInputError } from 'astro:actions';
|
||||
import { useState } from 'react';
|
||||
import {createLoggerFromFlags} from "../../../../../src/cli/flags.ts";
|
||||
|
||||
export function PostComment({
|
||||
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 { Logout } from '../../components/Logout';
|
||||
import { db, eq, Comment, Likes } from 'astro:db';
|
||||
|
@ -11,7 +11,7 @@ import { isInputError } from 'astro:actions';
|
|||
type Props = CollectionEntry<'blog'>;
|
||||
|
||||
const post = await getEntry('blog', Astro.params.slug)!;
|
||||
const { Content } = await post.render();
|
||||
const { Content } = await render(post);
|
||||
|
||||
if (Astro.url.searchParams.has('like')) {
|
||||
await Astro.callAction(actions.blog.like.orThrow, { postId: post.id });
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export const VIRTUAL_MODULE_ID = 'astro:actions';
|
||||
export const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
||||
export const ACTIONS_TYPES_FILE = 'actions.d.ts';
|
||||
export const VIRTUAL_INTERNAL_MODULE_ID = 'astro:internal-actions';
|
||||
export const RESOLVED_VIRTUAL_INTERNAL_MODULE_ID = '\0astro:internal-actions';
|
||||
export const ASTRO_ACTIONS_INTERNAL_MODULE_ID = 'astro-internal:actions';
|
||||
export const RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID = '\0' + ASTRO_ACTIONS_INTERNAL_MODULE_ID;
|
||||
export const NOOP_ACTIONS = '\0noop-actions';
|
||||
|
||||
export const ACTION_QUERY_PARAMS = {
|
||||
|
|
|
@ -17,18 +17,13 @@ export default function astroIntegrationActionsRouteHandler({
|
|||
return {
|
||||
name: VIRTUAL_MODULE_ID,
|
||||
hooks: {
|
||||
async 'astro:config:setup'(params) {
|
||||
async 'astro:config:setup'() {
|
||||
settings.injectedRoutes.push({
|
||||
pattern: ACTION_RPC_ROUTE_PATTERN,
|
||||
entrypoint: 'astro/actions/runtime/route.js',
|
||||
prerender: false,
|
||||
origin: 'internal',
|
||||
});
|
||||
|
||||
params.addMiddleware({
|
||||
entrypoint: 'astro/actions/runtime/middleware.js',
|
||||
order: 'post',
|
||||
});
|
||||
},
|
||||
'astro:config:done': async (params) => {
|
||||
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 {
|
||||
NOOP_ACTIONS,
|
||||
RESOLVED_VIRTUAL_INTERNAL_MODULE_ID,
|
||||
RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID,
|
||||
RESOLVED_VIRTUAL_MODULE_ID,
|
||||
VIRTUAL_INTERNAL_MODULE_ID,
|
||||
ASTRO_ACTIONS_INTERNAL_MODULE_ID,
|
||||
VIRTUAL_MODULE_ID,
|
||||
} from './consts.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`
|
||||
|
@ -24,7 +28,7 @@ export function vitePluginUserActions({ settings }: { settings: AstroSettings })
|
|||
if (id === NOOP_ACTIONS) {
|
||||
return NOOP_ACTIONS;
|
||||
}
|
||||
if (id === VIRTUAL_INTERNAL_MODULE_ID) {
|
||||
if (id === ASTRO_ACTIONS_INTERNAL_MODULE_ID) {
|
||||
const resolvedModule = await this.resolve(
|
||||
`${decodeURI(new URL('actions', settings.config.srcDir).pathname)}`,
|
||||
);
|
||||
|
@ -33,20 +37,50 @@ export function vitePluginUserActions({ settings }: { settings: AstroSettings })
|
|||
return NOOP_ACTIONS;
|
||||
}
|
||||
resolvedActionsId = resolvedModule.id;
|
||||
return RESOLVED_VIRTUAL_INTERNAL_MODULE_ID;
|
||||
return RESOLVED_ASTRO_ACTIONS_INTERNAL_MODULE_ID;
|
||||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (id === NOOP_ACTIONS) {
|
||||
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}';`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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({
|
||||
fs,
|
||||
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,
|
||||
} from '../utils.js';
|
||||
import type { Locals } from '../utils.js';
|
||||
import { getAction } from './get-action.js';
|
||||
import {
|
||||
ACTION_QUERY_PARAMS,
|
||||
ActionError,
|
||||
|
@ -239,7 +238,7 @@ function unwrapBaseObjectSchema(schema: z.ZodType, unparsedInput: FormData) {
|
|||
return schema;
|
||||
}
|
||||
|
||||
export type ActionMiddlewareContext = {
|
||||
export type AstroActionContext = {
|
||||
/** Information about an incoming action request. */
|
||||
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.
|
||||
*/
|
||||
export function getActionContext(context: APIContext): ActionMiddlewareContext {
|
||||
export function getActionContext(context: APIContext): AstroActionContext {
|
||||
const callerInfo = getCallerInfo(context);
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
action = {
|
||||
|
@ -291,7 +290,7 @@ export function getActionContext(context: APIContext): ActionMiddlewareContext {
|
|||
? removeTrailingForwardSlash(callerInfo.name)
|
||||
: callerInfo.name;
|
||||
|
||||
const baseAction = await getAction(callerInfoName);
|
||||
const baseAction = await pipeline.getAction(callerInfoName);
|
||||
let input;
|
||||
try {
|
||||
input = await parseRequestBody(context.request);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { parse as devalueParse, stringify as devalueStringify } from 'devalue';
|
||||
import type { z } from 'zod';
|
||||
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 { appendForwardSlash as _appendForwardSlash } from '../../../core/path.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,
|
||||
} from '../../types/public/internal.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;
|
||||
|
||||
|
@ -75,6 +77,7 @@ export type SSRManifest = {
|
|||
key: Promise<CryptoKey>;
|
||||
i18n: SSRManifestI18n | undefined;
|
||||
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
|
||||
actions?: SSRActions;
|
||||
checkOrigin: boolean;
|
||||
sessionConfig?: ResolvedSessionConfig<any>;
|
||||
cacheDir: string | URL;
|
||||
|
@ -85,6 +88,10 @@ export type SSRManifest = {
|
|||
buildServerDir: string | URL;
|
||||
};
|
||||
|
||||
export type SSRActions = {
|
||||
server: Record<string, ActionClient<unknown, ActionAccept, ZodType>>;
|
||||
};
|
||||
|
||||
export type SSRManifestI18n = {
|
||||
fallback: Record<string, string> | undefined;
|
||||
fallbackType: 'redirect' | 'rewrite';
|
||||
|
|
|
@ -14,6 +14,11 @@ import { NOOP_MIDDLEWARE_FN } from './middleware/noop-middleware.js';
|
|||
import { sequence } from './middleware/sequence.js';
|
||||
import { RouteCache } from './render/route-cache.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.
|
||||
|
@ -24,6 +29,7 @@ import { createDefaultRoutes } from './routing/default.js';
|
|||
export abstract class Pipeline {
|
||||
readonly internalMiddleware: MiddlewareHandler[];
|
||||
resolvedMiddleware: MiddlewareHandler | undefined = undefined;
|
||||
resolvedActions: SSRActions | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
readonly logger: Logger,
|
||||
|
@ -58,6 +64,8 @@ export abstract class Pipeline {
|
|||
* Used to find the route module
|
||||
*/
|
||||
readonly defaultRoutes = createDefaultRoutes(manifest),
|
||||
|
||||
readonly actions = manifest.actions,
|
||||
) {
|
||||
this.internalMiddleware = [];
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -27,7 +27,7 @@ import type {
|
|||
SSRError,
|
||||
SSRLoadedRenderer,
|
||||
} 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 { AstroError, AstroErrorData } from '../errors/index.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
|
||||
? await import(internals.middlewareEntryPoint.toString()).then((mod) => mod.onRequest)
|
||||
: NOOP_MIDDLEWARE_FN;
|
||||
|
||||
const actions: SSRActions = internals.astroActionsEntryPoint
|
||||
? await import(internals.astroActionsEntryPoint.toString()).then((mod) => mod)
|
||||
: { server: {} };
|
||||
manifest = createBuildManifest(
|
||||
options.settings,
|
||||
internals,
|
||||
renderers.renderers as SSRLoadedRenderer[],
|
||||
middleware,
|
||||
actions,
|
||||
options.key,
|
||||
);
|
||||
}
|
||||
|
@ -451,8 +456,7 @@ function getUrlForPath(
|
|||
removeTrailingForwardSlash(removeLeadingForwardSlash(pathname)) + ending;
|
||||
buildPathname = joinPaths(base, buildPathRelative);
|
||||
}
|
||||
const url = new URL(buildPathname, origin);
|
||||
return url;
|
||||
return new URL(buildPathname, origin);
|
||||
}
|
||||
|
||||
interface GeneratePathOptions {
|
||||
|
@ -599,6 +603,7 @@ function createBuildManifest(
|
|||
internals: BuildInternals,
|
||||
renderers: SSRLoadedRenderer[],
|
||||
middleware: MiddlewareHandler,
|
||||
actions: SSRActions,
|
||||
key: Promise<CryptoKey>,
|
||||
): SSRManifest {
|
||||
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
||||
|
@ -641,6 +646,7 @@ function createBuildManifest(
|
|||
onRequest: middleware,
|
||||
};
|
||||
},
|
||||
actions,
|
||||
checkOrigin:
|
||||
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
|
||||
key,
|
||||
|
|
|
@ -87,7 +87,8 @@ export interface BuildInternals {
|
|||
manifestFileName?: string;
|
||||
entryPoints: Map<RouteData, URL>;
|
||||
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
|
||||
|
@ -118,6 +119,8 @@ export function createBuildInternals(): BuildInternals {
|
|||
componentMetadata: new Map(),
|
||||
entryPoints: new Map(),
|
||||
prerenderOnlyChunks: [],
|
||||
astroActionsEntryPoint: undefined,
|
||||
middlewareEntryPoint: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { pluginPrerender } from './plugin-prerender.js';
|
|||
import { pluginRenderers } from './plugin-renderers.js';
|
||||
import { pluginScripts } from './plugin-scripts.js';
|
||||
import { pluginSSR } from './plugin-ssr.js';
|
||||
import { pluginActions } from './plugin-actions.js';
|
||||
|
||||
export function registerAllPlugins({ internals, options, register }: AstroBuildPluginContainer) {
|
||||
register(pluginComponentEntry(internals));
|
||||
|
@ -21,6 +22,7 @@ export function registerAllPlugins({ internals, options, register }: AstroBuildP
|
|||
register(pluginManifest(options, internals));
|
||||
register(pluginRenderers(options));
|
||||
register(pluginMiddleware(options, internals));
|
||||
register(pluginActions(options, internals));
|
||||
register(pluginPages(options, internals));
|
||||
register(pluginCSS(options, 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 { AstroBuildPlugin } from '../plugin.js';
|
||||
import type { StaticBuildOptions } from '../types.js';
|
||||
export { MIDDLEWARE_MODULE_ID } from '../../middleware/vite-plugin.js';
|
||||
|
||||
export function pluginMiddleware(
|
||||
opts: StaticBuildOptions,
|
||||
|
|
|
@ -7,10 +7,11 @@ import type { BuildInternals } from '../internal.js';
|
|||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
import type { StaticBuildOptions } from '../types.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 { RENDERERS_MODULE_ID } from './plugin-renderers.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 RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
||||
|
@ -167,6 +168,7 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
|
|||
|
||||
const imports = [
|
||||
`import { renderers } from '${RENDERERS_MODULE_ID}';`,
|
||||
`import * as actions from '${ASTRO_ACTIONS_INTERNAL_MODULE_ID}';`,
|
||||
`import * as serverEntrypointModule from '${ADAPTER_VIRTUAL_MODULE_ID}';`,
|
||||
`import { manifest as defaultManifest } from '${SSR_MANIFEST_VIRTUAL_MODULE_ID}';`,
|
||||
`import { serverIslandMap } from '${VIRTUAL_ISLAND_MAP_ID}';`,
|
||||
|
@ -178,6 +180,7 @@ function generateSSRCode(adapter: AstroAdapter, middlewareId: string) {
|
|||
` pageMap,`,
|
||||
` serverIslandMap,`,
|
||||
` renderers,`,
|
||||
` actions,`,
|
||||
` middleware: ${edgeMiddleware ? 'undefined' : `() => import("${middlewareId}")`}`,
|
||||
`});`,
|
||||
`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.',
|
||||
} 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
|
||||
* @see
|
||||
|
|
|
@ -32,6 +32,8 @@ import { type Pipeline, Slots, getParams, getProps } from './render/index.js';
|
|||
import { isRoute404or500, isRouteExternalRedirect, isRouteServerIsland } from './routing/match.js';
|
||||
import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rewrite.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');
|
||||
|
||||
|
@ -44,6 +46,7 @@ export class RenderContext {
|
|||
readonly pipeline: Pipeline,
|
||||
public locals: App.Locals,
|
||||
readonly middleware: MiddlewareHandler,
|
||||
readonly actions: SSRActions,
|
||||
// It must be a DECODED pathname
|
||||
public pathname: string,
|
||||
public request: Request,
|
||||
|
@ -80,16 +83,19 @@ export class RenderContext {
|
|||
status = 200,
|
||||
props,
|
||||
partial = undefined,
|
||||
actions
|
||||
}: Pick<RenderContext, 'pathname' | 'pipeline' | 'request' | 'routeData' | 'clientAddress'> &
|
||||
Partial<
|
||||
Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props' | 'partial'>
|
||||
Pick<RenderContext, 'locals' | 'middleware' | 'status' | 'props' | 'partial' | 'actions'>
|
||||
>): Promise<RenderContext> {
|
||||
const pipelineMiddleware = await pipeline.getMiddleware();
|
||||
const pipelineActions = actions ?? await pipeline.getActions();
|
||||
setOriginPathname(request, pathname);
|
||||
return new RenderContext(
|
||||
pipeline,
|
||||
locals,
|
||||
sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
|
||||
pipelineActions,
|
||||
pathname,
|
||||
request,
|
||||
routeData,
|
||||
|
@ -132,7 +138,8 @@ export class RenderContext {
|
|||
serverLike,
|
||||
base: manifest.base,
|
||||
});
|
||||
const apiContext = this.createAPIContext(props);
|
||||
const actionApiContext = this.createActionAPIContext();
|
||||
const apiContext = this.createAPIContext(props, actionApiContext);
|
||||
|
||||
this.counter++;
|
||||
if (this.counter === 4) {
|
||||
|
@ -192,6 +199,15 @@ export class RenderContext {
|
|||
}
|
||||
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) {
|
||||
case 'endpoint': {
|
||||
response = await renderEndpoint(
|
||||
|
@ -205,7 +221,7 @@ export class RenderContext {
|
|||
case 'redirect':
|
||||
return renderRedirect(this);
|
||||
case 'page': {
|
||||
const result = await this.createResult(componentInstance!);
|
||||
const result = await this.createResult(componentInstance!, actionApiContext);
|
||||
try {
|
||||
response = await renderPage(
|
||||
result,
|
||||
|
@ -263,8 +279,7 @@ export class RenderContext {
|
|||
return response;
|
||||
}
|
||||
|
||||
createAPIContext(props: APIContext['props']): APIContext {
|
||||
const context = this.createActionAPIContext();
|
||||
createAPIContext(props: APIContext['props'], context: ActionAPIContext): APIContext {
|
||||
const redirect = (path: string, status = 302) =>
|
||||
new Response(null, { status, headers: { Location: path } });
|
||||
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 { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } =
|
||||
pipeline;
|
||||
|
@ -403,7 +418,7 @@ export class RenderContext {
|
|||
cookies,
|
||||
/** This function returns the `Astro` faux-global */
|
||||
createAstro: (astroGlobal, props, slots) =>
|
||||
this.createAstro(result, astroGlobal, props, slots),
|
||||
this.createAstro(result, astroGlobal, props, slots, ctx),
|
||||
links,
|
||||
params: this.params,
|
||||
partial,
|
||||
|
@ -448,6 +463,7 @@ export class RenderContext {
|
|||
astroStaticPartial: AstroGlobalPartial,
|
||||
props: Record<string, any>,
|
||||
slotValues: Record<string, any> | null,
|
||||
apiContext: ActionAPIContext,
|
||||
): AstroGlobal {
|
||||
let astroPagePartial;
|
||||
// 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(
|
||||
result,
|
||||
astroStaticPartial,
|
||||
apiContext,
|
||||
);
|
||||
} else {
|
||||
// Create page partial with static partial so they can be cached together.
|
||||
astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial(
|
||||
result,
|
||||
astroStaticPartial,
|
||||
apiContext,
|
||||
);
|
||||
}
|
||||
// Create component-level partials. `Astro.self` is added by the compiler.
|
||||
|
@ -493,6 +511,7 @@ export class RenderContext {
|
|||
createAstroPagePartial(
|
||||
result: SSRResult,
|
||||
astroStaticPartial: AstroGlobalPartial,
|
||||
apiContext: ActionAPIContext,
|
||||
): Omit<AstroGlobal, 'props' | 'self' | 'slots'> {
|
||||
const renderContext = this;
|
||||
const { cookies, locals, params, pipeline, url, session } = this;
|
||||
|
@ -511,6 +530,8 @@ export class RenderContext {
|
|||
return await this.#executeRewrite(reroutePayload);
|
||||
};
|
||||
|
||||
const callAction = createCallAction(apiContext);
|
||||
|
||||
return {
|
||||
generator: astroStaticPartial.generator,
|
||||
glob: astroStaticPartial.glob,
|
||||
|
@ -539,7 +560,7 @@ export class RenderContext {
|
|||
site: pipeline.site,
|
||||
getActionResult: createGetActionResult(locals),
|
||||
get callAction() {
|
||||
return createCallAction(this);
|
||||
return callAction;
|
||||
},
|
||||
url,
|
||||
get originPathname() {
|
||||
|
|
|
@ -212,16 +212,4 @@ export class DevPipeline extends Pipeline {
|
|||
setManifestData(manifestData: RoutesList) {
|
||||
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 {
|
||||
return {
|
||||
name: 'astro:server',
|
||||
configureServer(viteServer) {
|
||||
async configureServer(viteServer) {
|
||||
const loader = createViteLoader(viteServer);
|
||||
const pipeline = DevPipeline.create(routesList, {
|
||||
loader,
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { ComponentInstance, RoutesList } from '../types/astro.js';
|
|||
import type { RouteData } from '../types/public/internal.js';
|
||||
import type { DevPipeline } from './pipeline.js';
|
||||
import { writeSSRResult, writeWebResponse } from './response.js';
|
||||
import {loadActions} from "../actions/loadActions.js";
|
||||
|
||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||
...args: any
|
||||
|
@ -159,6 +160,8 @@ export async function handleRoute({
|
|||
let renderContext: RenderContext;
|
||||
let mod: ComponentInstance | undefined = undefined;
|
||||
let route: RouteData;
|
||||
const actions = await loadActions(loader);
|
||||
pipeline.setActions(actions);
|
||||
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.
|
||||
const locals = Reflect.get(incomingRequest, clientLocalsSymbol);
|
||||
|
@ -192,6 +195,7 @@ export async function handleRoute({
|
|||
request,
|
||||
routeData: route,
|
||||
clientAddress: incomingRequest.socket.remoteAddress,
|
||||
actions
|
||||
});
|
||||
|
||||
let response;
|
||||
|
|
|
@ -4,8 +4,10 @@ import {
|
|||
appendForwardSlash,
|
||||
deserializeActionResult,
|
||||
getActionQueryString,
|
||||
astroCalledServerError,
|
||||
} from 'astro:actions';
|
||||
|
||||
const apiContextRoutesSymbol = Symbol.for('context.routes');
|
||||
const ENCODED_DOT = '%2E';
|
||||
|
||||
function toActionProxy(actionCallback = {}, aggregatedPath = '') {
|
||||
|
@ -73,11 +75,13 @@ export function getActionPath(action) {
|
|||
*/
|
||||
async function handleAction(param, path, context) {
|
||||
// When running server-side, import the action and call it.
|
||||
if (import.meta.env.SSR) {
|
||||
const { getAction } = await import('astro/actions/runtime/virtual/get-action.js');
|
||||
const action = await getAction(path);
|
||||
if (import.meta.env.SSR && context) {
|
||||
const pipeline = Reflect.get(context, apiContextRoutesSymbol);
|
||||
if (!pipeline) {
|
||||
throw astroCalledServerError();
|
||||
}
|
||||
const action = await pipeline.getAction(path);
|
||||
if (!action) throw new Error(`Action not found: ${path}`);
|
||||
|
||||
return action.bind(context)(param);
|
||||
}
|
||||
|
||||
|
|
|
@ -60,8 +60,8 @@ describe('Astro Actions', () => {
|
|||
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
||||
|
||||
const data = devalue.parse(await res.text());
|
||||
|
||||
assert.equal(data.channel, 'bholmesdev');
|
||||
assert.equal(data.subscribeButtonState, 'smashed');
|
||||
});
|
||||
|
@ -578,7 +578,6 @@ it('Should support trailing slash', async () => {
|
|||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import { actions } from "astro:actions";
|
||||
|
||||
// this is invalid, it should fail
|
||||
const result = await actions.subscribe({ channel: "hey" });
|
||||
---
|
||||
|
|
Loading…
Add table
Reference in a new issue