astro 6.3.1 → 6.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/endpoint/dev.js +1 -1
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/content/content-layer.js +3 -3
- package/dist/core/app/base.js +15 -7
- package/dist/core/build/index.js +7 -1
- package/dist/core/build/types.d.ts +0 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/csp/config.js +17 -5
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/errors/zod-error-map.js +3 -1
- package/dist/core/messages/runtime.js +1 -1
- package/dist/core/routing/handler.js +3 -5
- package/dist/core/util/normalized-url.js +5 -2
- package/dist/core/util/pathname.d.ts +10 -1
- package/dist/core/util/pathname.js +8 -1
- package/dist/environments.js +1 -1
- package/dist/manifest/virtual-module.js +1 -0
- package/dist/runtime/server/render/component.js +3 -3
- package/dist/runtime/server/render/util.js +1 -1
- package/dist/types/public/config.d.ts +13 -13
- package/dist/types/public/manifest.d.ts +1 -1
- package/dist/vite-plugin-app/app.d.ts +9 -1
- package/dist/vite-plugin-app/app.js +18 -1
- package/dist/vite-plugin-app/createAstroServerApp.d.ts +3 -1
- package/dist/vite-plugin-app/createAstroServerApp.js +4 -3
- package/dist/vite-plugin-astro-server/plugin.js +7 -2
- package/dist/vite-plugin-astro-server/route-guard.d.ts +33 -0
- package/dist/vite-plugin-astro-server/route-guard.js +42 -23
- package/package.json +3 -3
|
@@ -191,7 +191,7 @@ ${contentConfig.error.message}`
|
|
|
191
191
|
logger.info("Content config changed");
|
|
192
192
|
shouldClear = true;
|
|
193
193
|
}
|
|
194
|
-
if (previousAstroVersion && previousAstroVersion !== "6.3.
|
|
194
|
+
if (previousAstroVersion && previousAstroVersion !== "6.3.3") {
|
|
195
195
|
logger.info("Astro version changed");
|
|
196
196
|
shouldClear = true;
|
|
197
197
|
}
|
|
@@ -199,8 +199,8 @@ ${contentConfig.error.message}`
|
|
|
199
199
|
logger.info("Clearing content store");
|
|
200
200
|
this.#store.clearAll();
|
|
201
201
|
}
|
|
202
|
-
if ("6.3.
|
|
203
|
-
this.#store.metaStore().set("astro-version", "6.3.
|
|
202
|
+
if ("6.3.3") {
|
|
203
|
+
this.#store.metaStore().set("astro-version", "6.3.3");
|
|
204
204
|
}
|
|
205
205
|
if (currentConfigDigest) {
|
|
206
206
|
this.#store.metaStore().set("content-config-digest", currentConfigDigest);
|
package/dist/core/app/base.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DefaultFetchHandler } from "../fetch/default-handler.js";
|
|
|
16
16
|
import { appSymbol } from "../constants.js";
|
|
17
17
|
import { DefaultErrorHandler } from "../errors/default-handler.js";
|
|
18
18
|
import { setRenderOptions } from "./render-options.js";
|
|
19
|
+
import { MultiLevelEncodingError } from "../util/pathname.js";
|
|
19
20
|
class BaseApp {
|
|
20
21
|
manifest;
|
|
21
22
|
manifestData;
|
|
@@ -266,13 +267,20 @@ class BaseApp {
|
|
|
266
267
|
waitUntil
|
|
267
268
|
};
|
|
268
269
|
let response;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
270
|
+
try {
|
|
271
|
+
if (this.#fetchHandler instanceof DefaultFetchHandler) {
|
|
272
|
+
Reflect.set(request, appSymbol, this);
|
|
273
|
+
response = await this.#fetchHandler.renderWithOptions(request, resolvedOptions);
|
|
274
|
+
} else {
|
|
275
|
+
setRenderOptions(request, resolvedOptions);
|
|
276
|
+
Reflect.set(request, appSymbol, this);
|
|
277
|
+
response = await this.#fetchHandler.fetch(request);
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (err instanceof MultiLevelEncodingError) {
|
|
281
|
+
return new Response("Bad Request", { status: 400 });
|
|
282
|
+
}
|
|
283
|
+
throw err;
|
|
276
284
|
}
|
|
277
285
|
this.#warnMissingFeatures();
|
|
278
286
|
if (response.headers.get(ASTRO_ERROR_HEADER)) {
|
package/dist/core/build/index.js
CHANGED
|
@@ -151,7 +151,6 @@ class AstroBuilder {
|
|
|
151
151
|
runtimeMode: this.runtimeMode,
|
|
152
152
|
origin: this.origin,
|
|
153
153
|
pageNames,
|
|
154
|
-
teardownCompiler: this.teardownCompiler,
|
|
155
154
|
viteConfig,
|
|
156
155
|
key: keyPromise
|
|
157
156
|
};
|
|
@@ -203,6 +202,13 @@ class AstroBuilder {
|
|
|
203
202
|
} finally {
|
|
204
203
|
this.settings.timer.end("Total build");
|
|
205
204
|
this.settings.timer.writeStats();
|
|
205
|
+
if (this.teardownCompiler) {
|
|
206
|
+
try {
|
|
207
|
+
const { teardown } = await import("@astrojs/compiler");
|
|
208
|
+
teardown();
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
214
|
validateConfig() {
|
package/dist/core/constants.js
CHANGED
package/dist/core/csp/config.js
CHANGED
|
@@ -37,13 +37,25 @@ const ALLOWED_DIRECTIVES = [
|
|
|
37
37
|
"upgrade-insecure-requests",
|
|
38
38
|
"worker-src"
|
|
39
39
|
];
|
|
40
|
-
const allowedDirectivesSchema = z.custom((value) => {
|
|
41
|
-
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
return ALLOWED_DIRECTIVES.some((allowedValue) => {
|
|
40
|
+
const allowedDirectivesSchema = z.custom((v) => typeof v === "string").superRefine((value, ctx) => {
|
|
41
|
+
const isAllowed = ALLOWED_DIRECTIVES.some((allowedValue) => {
|
|
45
42
|
return value.startsWith(allowedValue);
|
|
46
43
|
});
|
|
44
|
+
if (!isAllowed) {
|
|
45
|
+
if (value.startsWith("script-src") || value.startsWith("style-src")) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: z.ZodIssueCode.custom,
|
|
48
|
+
message: `Directives \`script-src\` and \`style-src\` are not allowed in \`security.csp.directives\`. Please use \`security.csp.scriptDirective\` and \`security.csp.styleDirective\` instead.`,
|
|
49
|
+
fatal: true
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: z.ZodIssueCode.custom,
|
|
54
|
+
message: `Invalid directive: "${value}". Allowed directives are: ${ALLOWED_DIRECTIVES.join(", ")}`,
|
|
55
|
+
fatal: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
47
59
|
});
|
|
48
60
|
export {
|
|
49
61
|
ALGORITHMS,
|
package/dist/core/dev/dev.js
CHANGED
|
@@ -37,7 +37,7 @@ async function dev(inlineConfig) {
|
|
|
37
37
|
await telemetry.record([]);
|
|
38
38
|
const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
|
|
39
39
|
const logger = restart.container.logger;
|
|
40
|
-
const currentVersion = "6.3.
|
|
40
|
+
const currentVersion = "6.3.3";
|
|
41
41
|
const isPrerelease = currentVersion.includes("-");
|
|
42
42
|
if (!isPrerelease) {
|
|
43
43
|
try {
|
|
@@ -44,7 +44,9 @@ const errorMap = (issue) => {
|
|
|
44
44
|
return errorMap(_issue);
|
|
45
45
|
}
|
|
46
46
|
const relativePath = flattenErrorPath(_issue.path).replace(baseErrorPath, "").replace(leadingPeriod, "");
|
|
47
|
-
if ("
|
|
47
|
+
if (_issue.code === "custom" && _issue.message && _issue.message.includes("security.csp")) {
|
|
48
|
+
expectedShape.push(_issue.message);
|
|
49
|
+
} else if ("expected" in _issue && typeof _issue.expected === "string") {
|
|
48
50
|
expectedShape.push(
|
|
49
51
|
relativePath ? `${relativePath}: ${_issue.expected}` : _issue.expected
|
|
50
52
|
);
|
|
@@ -94,11 +94,9 @@ class AstroHandler {
|
|
|
94
94
|
state.status = defaultStatus;
|
|
95
95
|
let response;
|
|
96
96
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (sessionP || cacheP) await Promise.all([sessionP, cacheP]);
|
|
101
|
-
}
|
|
97
|
+
const sessionP = this.#hasSession ? provideSession(state) : void 0;
|
|
98
|
+
const cacheP = provideCache(state);
|
|
99
|
+
if (sessionP || cacheP) await Promise.all([sessionP, cacheP]);
|
|
102
100
|
state.pipeline.usedFeatures |= PipelineFeatures.sessions;
|
|
103
101
|
if (routeData.type === "redirect") {
|
|
104
102
|
const redirectResponse = await renderRedirect(state);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { collapseDuplicateSlashes } from "@astrojs/internal-helpers/path";
|
|
2
|
-
import { validateAndDecodePathname } from "./pathname.js";
|
|
2
|
+
import { MultiLevelEncodingError, validateAndDecodePathname } from "./pathname.js";
|
|
3
3
|
function createNormalizedUrl(requestUrl) {
|
|
4
4
|
return normalizeUrl(new URL(requestUrl));
|
|
5
5
|
}
|
|
6
6
|
function normalizeUrl(url) {
|
|
7
7
|
try {
|
|
8
8
|
url.pathname = validateAndDecodePathname(url.pathname);
|
|
9
|
-
} catch {
|
|
9
|
+
} catch (e) {
|
|
10
|
+
if (e instanceof MultiLevelEncodingError) {
|
|
11
|
+
throw e;
|
|
12
|
+
}
|
|
10
13
|
try {
|
|
11
14
|
url.pathname = decodeURI(url.pathname);
|
|
12
15
|
} catch {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when multi-level URL encoding is detected in a pathname.
|
|
3
|
+
* This is a distinct error type so callers can handle it specifically
|
|
4
|
+
* (e.g., returning a 400 response) rather than falling back to partial decoding.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MultiLevelEncodingError extends Error {
|
|
7
|
+
constructor();
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* Validates that a pathname is not multi-level encoded.
|
|
3
11
|
* Detects if a pathname contains encoding that was encoded again (e.g., %2561dmin where %25 decodes to %).
|
|
@@ -5,6 +13,7 @@
|
|
|
5
13
|
*
|
|
6
14
|
* @param pathname - The pathname to validate
|
|
7
15
|
* @returns The decoded pathname if valid
|
|
8
|
-
* @throws
|
|
16
|
+
* @throws MultiLevelEncodingError if multi-level encoding is detected
|
|
17
|
+
* @throws Error if the pathname contains invalid URL encoding
|
|
9
18
|
*/
|
|
10
19
|
export declare function validateAndDecodePathname(pathname: string): string;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
class MultiLevelEncodingError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("Multi-level URL encoding is not allowed");
|
|
4
|
+
this.name = "MultiLevelEncodingError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
1
7
|
function validateAndDecodePathname(pathname) {
|
|
2
8
|
let decoded;
|
|
3
9
|
try {
|
|
@@ -8,10 +14,11 @@ function validateAndDecodePathname(pathname) {
|
|
|
8
14
|
const hasDecoding = decoded !== pathname;
|
|
9
15
|
const decodedStillHasEncoding = /%[0-9a-fA-F]{2}/.test(decoded);
|
|
10
16
|
if (hasDecoding && decodedStillHasEncoding) {
|
|
11
|
-
throw new
|
|
17
|
+
throw new MultiLevelEncodingError();
|
|
12
18
|
}
|
|
13
19
|
return decoded;
|
|
14
20
|
}
|
|
15
21
|
export {
|
|
22
|
+
MultiLevelEncodingError,
|
|
16
23
|
validateAndDecodePathname
|
|
17
24
|
};
|
package/dist/environments.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "./core/constants.js";
|
|
2
2
|
function isAstroServerEnvironment(environment) {
|
|
3
|
-
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
|
|
3
|
+
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro;
|
|
4
4
|
}
|
|
5
5
|
function isAstroClientEnvironment(environment) {
|
|
6
6
|
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clsx } from "clsx";
|
|
2
2
|
import { AstroError, AstroErrorData } from "../../../core/errors/index.js";
|
|
3
|
-
import { markHTMLString } from "../escape.js";
|
|
3
|
+
import { escapeHTML, markHTMLString } from "../escape.js";
|
|
4
4
|
import { extractDirectives, generateHydrateScript } from "../hydration.js";
|
|
5
5
|
import { serializeProps } from "../serialize.js";
|
|
6
6
|
import { shorthash } from "../shorthash.js";
|
|
@@ -287,7 +287,7 @@ ${serializeProps(
|
|
|
287
287
|
if (Object.keys(children).length > 0) {
|
|
288
288
|
for (const key of Object.keys(children)) {
|
|
289
289
|
let tagName = renderer?.ssr?.supportsAstroStaticSlot ? !!metadata.hydrate ? "astro-slot" : "astro-static-slot" : "astro-slot";
|
|
290
|
-
let expectedHTML = key === "default" ? `<${tagName}>` : `<${tagName} name="${key}">`;
|
|
290
|
+
let expectedHTML = key === "default" ? `<${tagName}>` : `<${tagName} name="${escapeHTML(key)}">`;
|
|
291
291
|
if (!html.includes(expectedHTML)) {
|
|
292
292
|
unrenderedSlots.push(key);
|
|
293
293
|
}
|
|
@@ -297,7 +297,7 @@ ${serializeProps(
|
|
|
297
297
|
unrenderedSlots = Object.keys(children);
|
|
298
298
|
}
|
|
299
299
|
const template = unrenderedSlots.length > 0 ? unrenderedSlots.map(
|
|
300
|
-
(key) => `<template data-astro-template${key !== "default" ? `="${key}"` : ""}>${children[key]}</template>`
|
|
300
|
+
(key) => `<template data-astro-template${key !== "default" ? `="${escapeHTML(key)}"` : ""}>${children[key]}</template>`
|
|
301
301
|
).join("") : "";
|
|
302
302
|
island.children = `${html ?? ""}${template}`;
|
|
303
303
|
if (island.children) {
|
|
@@ -10,7 +10,7 @@ const toIdent = (k) => k.trim().replace(/(?!^)\b\w|\s+|\W+/g, (match, index) =>
|
|
|
10
10
|
if (/\W/.test(match)) return "";
|
|
11
11
|
return index === 0 ? match : match.toUpperCase();
|
|
12
12
|
});
|
|
13
|
-
const toAttributeString = (value, shouldEscape = true) => shouldEscape ? String(value).replace(AMPERSAND_REGEX, "
|
|
13
|
+
const toAttributeString = (value, shouldEscape = true) => shouldEscape ? String(value).replace(AMPERSAND_REGEX, "&").replace(DOUBLE_QUOTE_REGEX, """) : value;
|
|
14
14
|
const kebab = (k) => k.toLowerCase() === k ? k : k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
15
15
|
const toStyleString = (obj) => Object.entries(obj).filter(([_, v]) => typeof v === "string" && v.trim() || typeof v === "number").map(([k, v]) => {
|
|
16
16
|
if (k[0] !== "-" && k[1] !== "-") return `${kebab(k)}:${v}`;
|
|
@@ -1628,19 +1628,6 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
|
|
|
1628
1628
|
* ```
|
|
1629
1629
|
*/
|
|
1630
1630
|
service?: ImageServiceConfig;
|
|
1631
|
-
/**
|
|
1632
|
-
* @docs
|
|
1633
|
-
* @name image.dangerouslyProcessSVG
|
|
1634
|
-
* @type {boolean}
|
|
1635
|
-
* @default `false`
|
|
1636
|
-
* @version 6.3.0
|
|
1637
|
-
* @description
|
|
1638
|
-
*
|
|
1639
|
-
* Allows SVG source images to be processed by the image optimization pipeline.
|
|
1640
|
-
*
|
|
1641
|
-
* This is disabled by default as specifically formed SVGs can be prohibitively expensive to process and used by malicious actors to execute denial of service attacks. Only enable this option if you trust the source of your SVG images and understand the risks of processing them.
|
|
1642
|
-
*/
|
|
1643
|
-
dangerouslyProcessSVG?: boolean;
|
|
1644
1631
|
/**
|
|
1645
1632
|
* @docs
|
|
1646
1633
|
* @name image.service.config.limitInputPixels
|
|
@@ -1723,6 +1710,19 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
|
|
|
1723
1710
|
* This can be used for options such as `compressionLevel`, `effort`, `palette`, or a default `quality`.
|
|
1724
1711
|
* Per-image `quality` values from `<Image />`, `<Picture />`, and `getImage()` still take precedence.
|
|
1725
1712
|
*/
|
|
1713
|
+
/**
|
|
1714
|
+
* @docs
|
|
1715
|
+
* @name image.dangerouslyProcessSVG
|
|
1716
|
+
* @type {boolean}
|
|
1717
|
+
* @default `false`
|
|
1718
|
+
* @version 6.3.0
|
|
1719
|
+
* @description
|
|
1720
|
+
*
|
|
1721
|
+
* Allows SVG source images to be processed by the image optimization pipeline.
|
|
1722
|
+
*
|
|
1723
|
+
* This is disabled by default as specifically formed SVGs can be prohibitively expensive to process and used by malicious actors to execute denial of service attacks. Only enable this option if you trust the source of your SVG images and understand the risks of processing them.
|
|
1724
|
+
*/
|
|
1725
|
+
dangerouslyProcessSVG?: boolean;
|
|
1726
1726
|
/**
|
|
1727
1727
|
* @docs
|
|
1728
1728
|
* @name image.domains
|
|
@@ -13,7 +13,7 @@ type Dirs = Pick<SSRManifest, 'cacheDir' | 'outDir' | 'publicDir' | 'srcDir'>;
|
|
|
13
13
|
type DeserializedDirs = Extend<Dirs, URL>;
|
|
14
14
|
export type ServerDeserializedManifest = Pick<SSRManifest, 'base' | 'trailingSlash' | 'compressHTML' | 'site'> & DeserializedDirs & {
|
|
15
15
|
i18n: AstroConfig['i18n'];
|
|
16
|
-
build: Pick<AstroConfig['build'], 'server' | 'client' | 'format'>;
|
|
16
|
+
build: Pick<AstroConfig['build'], 'server' | 'client' | 'format' | 'assetsPrefix'>;
|
|
17
17
|
root: URL;
|
|
18
18
|
image: Pick<AstroConfig['image'], 'objectFit' | 'objectPosition' | 'layout'>;
|
|
19
19
|
};
|
|
@@ -33,7 +33,13 @@ export declare class AstroServerApp extends BaseApp<RunnablePipeline> {
|
|
|
33
33
|
devMatch(pathname: string): Promise<DevMatch | undefined>;
|
|
34
34
|
static create(manifest: SSRManifest, routesList: RoutesList, logger: AstroLogger, loader: ModuleLoader, settings: AstroSettings, getDebugInfo: () => Promise<string>): Promise<AstroServerApp>;
|
|
35
35
|
createPipeline(_streaming: boolean, manifest: SSRManifest, settings: AstroSettings, logger: AstroLogger, loader: ModuleLoader, manifestData: RoutesList, getDebugInfo: () => Promise<string>): RunnablePipeline;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Handle a request.
|
|
38
|
+
* @returns The return value indicates whether or not the request was handled
|
|
39
|
+
* by this handler. If the result is not `true`, then the request has not
|
|
40
|
+
* been handled yet and other handlers can be run.
|
|
41
|
+
*/
|
|
42
|
+
handleRequest({ controller, incomingRequest, incomingResponse, isHttps, prerenderOnly, }: HandleRequest): Promise<boolean>;
|
|
37
43
|
match(request: Request, _allowPrerenderedRoutes: boolean): RouteData | undefined;
|
|
38
44
|
protected createErrorHandler(): ErrorHandler;
|
|
39
45
|
logRequest({ pathname, method, statusCode, isRewrite, reqTime }: LogRequestPayload): void;
|
|
@@ -43,5 +49,7 @@ type HandleRequest = {
|
|
|
43
49
|
incomingRequest: http.IncomingMessage;
|
|
44
50
|
incomingResponse: http.ServerResponse;
|
|
45
51
|
isHttps: boolean;
|
|
52
|
+
/** When true, only handle prerendered routes. Returns false for SSR routes. */
|
|
53
|
+
prerenderOnly?: boolean;
|
|
46
54
|
};
|
|
47
55
|
export {};
|
|
@@ -92,11 +92,18 @@ class AstroServerApp extends BaseApp {
|
|
|
92
92
|
});
|
|
93
93
|
return pipeline;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Handle a request.
|
|
97
|
+
* @returns The return value indicates whether or not the request was handled
|
|
98
|
+
* by this handler. If the result is not `true`, then the request has not
|
|
99
|
+
* been handled yet and other handlers can be run.
|
|
100
|
+
*/
|
|
95
101
|
async handleRequest({
|
|
96
102
|
controller,
|
|
97
103
|
incomingRequest,
|
|
98
104
|
incomingResponse,
|
|
99
|
-
isHttps
|
|
105
|
+
isHttps,
|
|
106
|
+
prerenderOnly
|
|
100
107
|
}) {
|
|
101
108
|
const validated = validateForwardedHeaders(
|
|
102
109
|
getFirstForwardedValue(incomingRequest.headers["x-forwarded-proto"]),
|
|
@@ -131,14 +138,23 @@ class AstroServerApp extends BaseApp {
|
|
|
131
138
|
}
|
|
132
139
|
const self = this;
|
|
133
140
|
await self.#loadFetchHandler();
|
|
141
|
+
let handled = true;
|
|
134
142
|
await runWithErrorHandling({
|
|
135
143
|
controller,
|
|
136
144
|
pathname,
|
|
137
145
|
async run() {
|
|
138
146
|
const matchedRoute = await self.devMatch(pathname);
|
|
139
147
|
if (!matchedRoute) {
|
|
148
|
+
if (prerenderOnly) {
|
|
149
|
+
handled = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
140
152
|
throw new Error("No route matched, and default 404 route was not found.");
|
|
141
153
|
}
|
|
154
|
+
if (prerenderOnly && !matchedRoute.routeData.prerender) {
|
|
155
|
+
handled = false;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
142
158
|
const request = createRequest({
|
|
143
159
|
url,
|
|
144
160
|
headers: incomingRequest.headers,
|
|
@@ -174,6 +190,7 @@ class AstroServerApp extends BaseApp {
|
|
|
174
190
|
return error;
|
|
175
191
|
}
|
|
176
192
|
});
|
|
193
|
+
return handled;
|
|
177
194
|
}
|
|
178
195
|
match(request, _allowPrerenderedRoutes) {
|
|
179
196
|
return super.match(request, true);
|
|
@@ -4,5 +4,7 @@ import type { ModuleLoader } from '../core/module-loader/index.js';
|
|
|
4
4
|
import type { AstroSettings } from '../types/astro.js';
|
|
5
5
|
import type { DevServerController } from '../vite-plugin-astro-server/controller.js';
|
|
6
6
|
export default function createAstroServerApp(controller: DevServerController, settings: AstroSettings, loader: ModuleLoader, logger?: AstroLogger): Promise<{
|
|
7
|
-
handler(incomingRequest: http.IncomingMessage, incomingResponse: http.ServerResponse
|
|
7
|
+
handler(incomingRequest: http.IncomingMessage, incomingResponse: http.ServerResponse, options?: {
|
|
8
|
+
prerenderOnly?: boolean;
|
|
9
|
+
}): Promise<boolean>;
|
|
8
10
|
}>;
|
|
@@ -60,12 +60,13 @@ async function createAstroServerApp(controller, settings, loader, logger) {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
return {
|
|
63
|
-
handler(incomingRequest, incomingResponse) {
|
|
64
|
-
app.handleRequest({
|
|
63
|
+
handler(incomingRequest, incomingResponse, options) {
|
|
64
|
+
return app.handleRequest({
|
|
65
65
|
controller,
|
|
66
66
|
incomingRequest,
|
|
67
67
|
incomingResponse,
|
|
68
|
-
isHttps: loader?.isHttps() ?? false
|
|
68
|
+
isHttps: loader?.isHttps() ?? false,
|
|
69
|
+
prerenderOnly: options?.prerenderOnly
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
};
|
|
@@ -113,9 +113,14 @@ function createVitePluginAstroServer({
|
|
|
113
113
|
if (!matches.some((route) => route.prerender)) {
|
|
114
114
|
return next();
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const handled = await new Promise((resolve) => {
|
|
117
|
+
localStorage.run(request, () => {
|
|
118
|
+
prerenderHandler.handler(request, response, { prerenderOnly: true }).then((result) => resolve(result)).catch(() => resolve(true));
|
|
119
|
+
});
|
|
118
120
|
});
|
|
121
|
+
if (!handled) {
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
119
124
|
} catch (err) {
|
|
120
125
|
next(err);
|
|
121
126
|
}
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import type * as vite from 'vite';
|
|
2
2
|
import type { AstroSettings } from '../types/astro.js';
|
|
3
|
+
/**
|
|
4
|
+
* Outcome of the route guard evaluation for a dev-server request.
|
|
5
|
+
*
|
|
6
|
+
* - **`next`** — Allow the request through to downstream middleware.
|
|
7
|
+
* - **`block`** — The file exists at the project root but outside srcDir/publicDir.
|
|
8
|
+
* Respond with a 404.
|
|
9
|
+
*/
|
|
10
|
+
export type RouteGuardDecision = {
|
|
11
|
+
action: 'next';
|
|
12
|
+
} | {
|
|
13
|
+
action: 'block';
|
|
14
|
+
pathname: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Filesystem query results needed by the route guard decision function.
|
|
18
|
+
* Callers resolve these from the real filesystem; tests can provide them directly.
|
|
19
|
+
*/
|
|
20
|
+
export interface RouteGuardFsInfo {
|
|
21
|
+
/** Whether the resolved pathname exists inside the project's `publicDir` (e.g. `public/robots.txt`). */
|
|
22
|
+
existsInPublic: boolean;
|
|
23
|
+
/** Whether the resolved pathname exists inside the project's `srcDir` (e.g. `src/pages/index.astro`). */
|
|
24
|
+
existsInSrc: boolean;
|
|
25
|
+
/** Whether the resolved pathname exists at the project root as a **file** (not a directory). Directories are allowed through because they may share names with valid page routes. */
|
|
26
|
+
existsAtRootAsFile: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pure decision function for the route guard middleware.
|
|
30
|
+
*
|
|
31
|
+
* Determines whether a request should be blocked (file exists at project root
|
|
32
|
+
* but outside srcDir/publicDir) or allowed through. The filesystem lookups are
|
|
33
|
+
* injected via `fsInfo` so this function remains pure and unit-testable.
|
|
34
|
+
*/
|
|
35
|
+
export declare function evaluateRouteGuard(url: string, acceptHeader: string, fsInfo: RouteGuardFsInfo): RouteGuardDecision;
|
|
3
36
|
/**
|
|
4
37
|
* Middleware that prevents Vite from serving files that exist outside
|
|
5
38
|
* of srcDir and publicDir when accessed via direct URL navigation.
|
|
@@ -10,6 +10,30 @@ const VITE_INTERNAL_PREFIXES = [
|
|
|
10
10
|
"/node_modules/",
|
|
11
11
|
"/.astro/"
|
|
12
12
|
];
|
|
13
|
+
function evaluateRouteGuard(url, acceptHeader, fsInfo) {
|
|
14
|
+
if (!acceptHeader.includes("text/html")) {
|
|
15
|
+
return { action: "next" };
|
|
16
|
+
}
|
|
17
|
+
let pathname;
|
|
18
|
+
try {
|
|
19
|
+
pathname = decodeURI(new URL(url, "http://localhost").pathname);
|
|
20
|
+
} catch {
|
|
21
|
+
return { action: "next" };
|
|
22
|
+
}
|
|
23
|
+
if (VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
|
|
24
|
+
return { action: "next" };
|
|
25
|
+
}
|
|
26
|
+
if (url.includes("?")) {
|
|
27
|
+
return { action: "next" };
|
|
28
|
+
}
|
|
29
|
+
if (fsInfo.existsInPublic || fsInfo.existsInSrc) {
|
|
30
|
+
return { action: "next" };
|
|
31
|
+
}
|
|
32
|
+
if (fsInfo.existsAtRootAsFile) {
|
|
33
|
+
return { action: "block", pathname };
|
|
34
|
+
}
|
|
35
|
+
return { action: "next" };
|
|
36
|
+
}
|
|
13
37
|
function routeGuardMiddleware(settings) {
|
|
14
38
|
const { config } = settings;
|
|
15
39
|
return function devRouteGuard(req, res, next) {
|
|
@@ -18,41 +42,36 @@ function routeGuardMiddleware(settings) {
|
|
|
18
42
|
return next();
|
|
19
43
|
}
|
|
20
44
|
const accept = req.headers.accept || "";
|
|
21
|
-
if (!accept.includes("text/html")) {
|
|
22
|
-
return next();
|
|
23
|
-
}
|
|
24
45
|
let pathname;
|
|
25
46
|
try {
|
|
26
47
|
pathname = decodeURI(new URL(url, "http://localhost").pathname);
|
|
27
48
|
} catch {
|
|
28
49
|
return next();
|
|
29
50
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (fs.existsSync(srcFilePath)) {
|
|
42
|
-
return next();
|
|
51
|
+
const fsInfo = {
|
|
52
|
+
existsInPublic: fs.existsSync(new URL("." + pathname, config.publicDir)),
|
|
53
|
+
existsInSrc: fs.existsSync(new URL("." + pathname, config.srcDir)),
|
|
54
|
+
existsAtRootAsFile: false
|
|
55
|
+
};
|
|
56
|
+
if (accept.includes("text/html") && !url.includes("?") && !VITE_INTERNAL_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.statSync(new URL("." + pathname, config.root));
|
|
59
|
+
fsInfo.existsAtRootAsFile = stat.isFile();
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
43
62
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const html = notFoundTemplate(pathname);
|
|
63
|
+
const decision = evaluateRouteGuard(url, accept, fsInfo);
|
|
64
|
+
switch (decision.action) {
|
|
65
|
+
case "block": {
|
|
66
|
+
const html = notFoundTemplate(decision.pathname);
|
|
49
67
|
return writeHtmlResponse(res, 404, html);
|
|
50
68
|
}
|
|
51
|
-
|
|
69
|
+
case "next":
|
|
70
|
+
return next();
|
|
52
71
|
}
|
|
53
|
-
return next();
|
|
54
72
|
};
|
|
55
73
|
}
|
|
56
74
|
export {
|
|
75
|
+
evaluateRouteGuard,
|
|
57
76
|
routeGuardMiddleware
|
|
58
77
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.3",
|
|
4
4
|
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "withastro",
|
|
@@ -164,8 +164,8 @@
|
|
|
164
164
|
"xxhash-wasm": "^1.1.0",
|
|
165
165
|
"yargs-parser": "^22.0.0",
|
|
166
166
|
"zod": "^4.3.6",
|
|
167
|
-
"@astrojs/
|
|
168
|
-
"@astrojs/
|
|
167
|
+
"@astrojs/markdown-remark": "7.1.2",
|
|
168
|
+
"@astrojs/internal-helpers": "0.9.1",
|
|
169
169
|
"@astrojs/telemetry": "3.3.2"
|
|
170
170
|
},
|
|
171
171
|
"optionalDependencies": {
|