astro 7.0.5 → 7.0.6
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/components/Picture.astro +19 -2
- package/dist/actions/handler.js +7 -0
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/cli/install-package.js +5 -3
- package/dist/container/index.js +4 -2
- package/dist/content/content-layer.js +3 -3
- package/dist/core/app/origin-check.d.ts +32 -0
- package/dist/core/app/origin-check.js +52 -0
- package/dist/core/base-pipeline.js +1 -1
- package/dist/core/constants.js +1 -1
- package/dist/core/dev/dev.js +1 -1
- package/dist/core/messages/runtime.js +1 -1
- package/dist/core/pages/handler.js +9 -1
- package/dist/core/routing/generator.js +2 -2
- package/dist/core/util.d.ts +8 -0
- package/dist/core/util.js +23 -0
- package/dist/core/viteUtils.d.ts +3 -0
- package/dist/core/viteUtils.js +8 -2
- package/dist/runtime/server/render/common.js +7 -1
- package/dist/runtime/server/render/dom.js +4 -1
- package/dist/runtime/server/render/instruction.d.ts +1 -0
- package/dist/runtime/server/render/instruction.js +5 -1
- package/dist/runtime/server/render/server-islands.js +6 -6
- package/dist/runtime/server/render/slot.d.ts +15 -2
- package/dist/runtime/server/render/slot.js +37 -6
- package/dist/runtime/server/render/util.d.ts +1 -0
- package/dist/runtime/server/render/util.js +1 -0
- package/dist/vite-plugin-astro-server/plugin.js +3 -0
- package/dist/vite-plugin-css/index.js +1 -1
- package/package.json +5 -5
- package/templates/content/types.d.ts +4 -0
- package/dist/core/app/middlewares.d.ts +0 -8
- package/dist/core/app/middlewares.js +0 -49
package/components/Picture.astro
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getImage,
|
|
4
|
+
imageConfig,
|
|
5
|
+
inferRemoteSize,
|
|
6
|
+
type LocalImageProps,
|
|
7
|
+
type RemoteImageProps,
|
|
8
|
+
} from 'astro:assets';
|
|
3
9
|
import * as mime from 'mrmime';
|
|
4
|
-
import { isESMImportedImage, resolveSrc } from '../dist/assets/utils/imageKind.js';
|
|
10
|
+
import { isESMImportedImage, isRemoteImage, resolveSrc } from '../dist/assets/utils/imageKind.js';
|
|
5
11
|
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
|
|
6
12
|
import type {
|
|
7
13
|
GetImageResult,
|
|
@@ -62,6 +68,17 @@ for (const key in props) {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
const originalSrc = await resolveSrc(props.src);
|
|
71
|
+
|
|
72
|
+
// When inferSize is set for a remote image, resolve dimensions once and thread them through
|
|
73
|
+
// to all getImage() calls. This avoids multiple redundant HTTP requests to the same URL,
|
|
74
|
+
// which can trigger rate-limiting (HTTP 429) on servers like Wikimedia.
|
|
75
|
+
if (props.inferSize && isRemoteImage(originalSrc)) {
|
|
76
|
+
const remoteSize = await inferRemoteSize(originalSrc);
|
|
77
|
+
delete props.inferSize;
|
|
78
|
+
props.width ??= remoteSize.width;
|
|
79
|
+
props.height ??= remoteSize.height;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
const optimizedImages: GetImageResult[] = await Promise.all(
|
|
66
83
|
formats.map(
|
|
67
84
|
async (format) =>
|
package/dist/actions/handler.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCrossOriginForbiddenResponse,
|
|
3
|
+
isForbiddenCrossOriginRequest
|
|
4
|
+
} from "../core/app/origin-check.js";
|
|
1
5
|
import { PipelineFeatures } from "../core/base-pipeline.js";
|
|
2
6
|
import { getActionContext, serializeActionResult } from "./runtime/server.js";
|
|
3
7
|
class ActionHandler {
|
|
@@ -18,6 +22,9 @@ class ActionHandler {
|
|
|
18
22
|
if (!action) {
|
|
19
23
|
return void 0;
|
|
20
24
|
}
|
|
25
|
+
if (state.pipeline.manifest.checkOrigin && isForbiddenCrossOriginRequest(apiContext.request, apiContext.url, apiContext.isPrerendered)) {
|
|
26
|
+
return Promise.resolve(createCrossOriginForbiddenResponse(apiContext.request));
|
|
27
|
+
}
|
|
21
28
|
return this.#executeAction(action, setActionResult);
|
|
22
29
|
}
|
|
23
30
|
async #executeAction(action, setActionResult) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
import * as clack from "@clack/prompts";
|
|
3
4
|
import ci from "ci-info";
|
|
4
5
|
import { detect, resolveCommand } from "package-manager-detector";
|
|
@@ -8,8 +9,8 @@ const require2 = createRequire(import.meta.url);
|
|
|
8
9
|
const { bold, cyan, dim, magenta } = colors;
|
|
9
10
|
async function getPackage(packageName, logger, options, otherDeps = []) {
|
|
10
11
|
try {
|
|
11
|
-
require2.resolve(packageName, { paths: [options.cwd ?? process.cwd()] });
|
|
12
|
-
const packageImport = await import(
|
|
12
|
+
const resolved = require2.resolve(packageName, { paths: [options.cwd ?? process.cwd()] });
|
|
13
|
+
const packageImport = await import(pathToFileURL(resolved).href);
|
|
13
14
|
return packageImport;
|
|
14
15
|
} catch {
|
|
15
16
|
if (options.optional) return void 0;
|
|
@@ -25,7 +26,8 @@ async function getPackage(packageName, logger, options, otherDeps = []) {
|
|
|
25
26
|
}
|
|
26
27
|
const result = await installPackage([packageName, ...otherDeps], options, logger);
|
|
27
28
|
if (result) {
|
|
28
|
-
const
|
|
29
|
+
const resolved = require2.resolve(packageName, { paths: [options.cwd ?? process.cwd()] });
|
|
30
|
+
const packageImport = await import(pathToFileURL(resolved).href);
|
|
29
31
|
return packageImport;
|
|
30
32
|
} else {
|
|
31
33
|
return void 0;
|
package/dist/container/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { validateSegment } from "../core/routing/segment.js";
|
|
|
14
14
|
import { SlotString } from "../runtime/server/render/slot.js";
|
|
15
15
|
import { ContainerPipeline } from "./pipeline.js";
|
|
16
16
|
import { createConsoleLogger } from "../core/logger/impls/console.js";
|
|
17
|
+
const { gfm: _, smartypants: __, ...containerMarkdownDefaults } = ASTRO_CONFIG_DEFAULTS.markdown;
|
|
18
|
+
const CONTAINER_CONFIG_DEFAULTS = { ...ASTRO_CONFIG_DEFAULTS, markdown: containerMarkdownDefaults };
|
|
17
19
|
function createManifest(manifest, renderers, middleware) {
|
|
18
20
|
function middlewareInstance() {
|
|
19
21
|
return {
|
|
@@ -113,7 +115,7 @@ class experimental_AstroContainer {
|
|
|
113
115
|
*/
|
|
114
116
|
static async create(containerOptions = {}) {
|
|
115
117
|
const { streaming = false, manifest, renderers = [], resolve } = containerOptions;
|
|
116
|
-
const astroConfig = await validateConfig(
|
|
118
|
+
const astroConfig = await validateConfig(CONTAINER_CONFIG_DEFAULTS, process.cwd(), "container");
|
|
117
119
|
return new experimental_AstroContainer({
|
|
118
120
|
streaming,
|
|
119
121
|
manifest,
|
|
@@ -206,7 +208,7 @@ class experimental_AstroContainer {
|
|
|
206
208
|
// NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
|
|
207
209
|
// @ts-expect-error @ematipico: I plan to use it for a possible integration that could help people
|
|
208
210
|
static async createFromManifest(manifest) {
|
|
209
|
-
const astroConfig = await validateConfig(
|
|
211
|
+
const astroConfig = await validateConfig(CONTAINER_CONFIG_DEFAULTS, process.cwd(), "container");
|
|
210
212
|
const container = new experimental_AstroContainer({
|
|
211
213
|
manifest,
|
|
212
214
|
astroConfig
|
|
@@ -197,7 +197,7 @@ ${contentConfig.error.message}`
|
|
|
197
197
|
logger.info("Content config changed");
|
|
198
198
|
shouldClear = true;
|
|
199
199
|
}
|
|
200
|
-
if (previousAstroVersion && previousAstroVersion !== "7.0.
|
|
200
|
+
if (previousAstroVersion && previousAstroVersion !== "7.0.6") {
|
|
201
201
|
logger.info("Astro version changed");
|
|
202
202
|
shouldClear = true;
|
|
203
203
|
}
|
|
@@ -205,8 +205,8 @@ ${contentConfig.error.message}`
|
|
|
205
205
|
logger.info("Clearing content store");
|
|
206
206
|
this.#store.clearAll();
|
|
207
207
|
}
|
|
208
|
-
if ("7.0.
|
|
209
|
-
this.#store.metaStore().set("astro-version", "7.0.
|
|
208
|
+
if ("7.0.6") {
|
|
209
|
+
this.#store.metaStore().set("astro-version", "7.0.6");
|
|
210
210
|
}
|
|
211
211
|
if (currentConfigDigest) {
|
|
212
212
|
this.#store.metaStore().set("content-config-digest", currentConfigDigest);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home of the `security.checkOrigin` logic. Exposes the shared predicate and
|
|
3
|
+
* response used to reject cross-site submissions, plus the middleware factory
|
|
4
|
+
* that installs the check in the request pipeline. Consumers (the pipeline
|
|
5
|
+
* middleware and the Astro Actions dispatch) import from here so the check
|
|
6
|
+
* stays consistent regardless of where it runs.
|
|
7
|
+
*/
|
|
8
|
+
import type { MiddlewareHandler } from '../../types/public/common.js';
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether a request should be rejected because it is a cross-site
|
|
11
|
+
* submission for a route rendered on demand.
|
|
12
|
+
*
|
|
13
|
+
* This encapsulates the shared logic used by both the origin-check middleware
|
|
14
|
+
* and the Astro Actions dispatch, so the check is applied consistently
|
|
15
|
+
* regardless of where the request is handled.
|
|
16
|
+
*
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
export declare function isForbiddenCrossOriginRequest(request: Request, url: URL, isPrerendered: boolean): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Builds the 403 response returned when a cross-site submission is rejected.
|
|
22
|
+
*
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
export declare function createCrossOriginForbiddenResponse(request: Request): Response;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a middleware function in charge to check the `origin` header.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
export declare function createOriginCheckMiddleware(): MiddlewareHandler;
|
|
32
|
+
export declare function hasFormLikeHeader(contentType: string | null): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineMiddleware } from "../middleware/defineMiddleware.js";
|
|
2
|
+
const FORM_CONTENT_TYPES = [
|
|
3
|
+
"application/x-www-form-urlencoded",
|
|
4
|
+
"multipart/form-data",
|
|
5
|
+
"text/plain"
|
|
6
|
+
];
|
|
7
|
+
const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
8
|
+
function isForbiddenCrossOriginRequest(request, url, isPrerendered) {
|
|
9
|
+
if (isPrerendered) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (SAFE_METHODS.includes(request.method)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const isSameOrigin = request.headers.get("origin") === url.origin;
|
|
16
|
+
const hasContentType = request.headers.has("content-type");
|
|
17
|
+
if (hasContentType) {
|
|
18
|
+
const formLikeHeader = hasFormLikeHeader(request.headers.get("content-type"));
|
|
19
|
+
return formLikeHeader && !isSameOrigin;
|
|
20
|
+
}
|
|
21
|
+
return !isSameOrigin;
|
|
22
|
+
}
|
|
23
|
+
function createCrossOriginForbiddenResponse(request) {
|
|
24
|
+
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
25
|
+
status: 403
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function createOriginCheckMiddleware() {
|
|
29
|
+
return defineMiddleware((context, next) => {
|
|
30
|
+
const { request, url, isPrerendered } = context;
|
|
31
|
+
if (isForbiddenCrossOriginRequest(request, url, isPrerendered)) {
|
|
32
|
+
return createCrossOriginForbiddenResponse(request);
|
|
33
|
+
}
|
|
34
|
+
return next();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function hasFormLikeHeader(contentType) {
|
|
38
|
+
if (contentType) {
|
|
39
|
+
for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
|
|
40
|
+
if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
createCrossOriginForbiddenResponse,
|
|
49
|
+
createOriginCheckMiddleware,
|
|
50
|
+
hasFormLikeHeader,
|
|
51
|
+
isForbiddenCrossOriginRequest
|
|
52
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NOOP_ACTIONS_MOD } from "../actions/noop-actions.js";
|
|
2
|
-
import { createOriginCheckMiddleware } from "./app/
|
|
2
|
+
import { createOriginCheckMiddleware } from "./app/origin-check.js";
|
|
3
3
|
import { ActionNotFoundError } from "./errors/errors-data.js";
|
|
4
4
|
import { AstroError } from "./errors/index.js";
|
|
5
5
|
import { NOOP_MIDDLEWARE_FN } from "./middleware/noop-middleware.js";
|
package/dist/core/constants.js
CHANGED
package/dist/core/dev/dev.js
CHANGED
|
@@ -26,7 +26,7 @@ async function dev(inlineConfig) {
|
|
|
26
26
|
await telemetry.record([]);
|
|
27
27
|
const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
|
|
28
28
|
const logger = restart.container.logger;
|
|
29
|
-
const currentVersion = "7.0.
|
|
29
|
+
const currentVersion = "7.0.6";
|
|
30
30
|
const isPrerelease = currentVersion.includes("-");
|
|
31
31
|
if (!isPrerelease) {
|
|
32
32
|
try {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { renderEndpoint } from "../../runtime/server/endpoint.js";
|
|
2
2
|
import { renderPage } from "../../runtime/server/index.js";
|
|
3
3
|
import { ASTRO_ERROR_HEADER } from "../constants.js";
|
|
4
|
+
import {
|
|
5
|
+
createCrossOriginForbiddenResponse,
|
|
6
|
+
isForbiddenCrossOriginRequest
|
|
7
|
+
} from "../app/origin-check.js";
|
|
4
8
|
import { getCookiesFromResponse } from "../cookies/response.js";
|
|
5
9
|
const EMPTY_SLOTS = Object.freeze({});
|
|
6
10
|
class PagesHandler {
|
|
@@ -78,8 +82,12 @@ class PagesHandler {
|
|
|
78
82
|
if (!state.routeData) {
|
|
79
83
|
return new Response(null, { status: 404, headers: { [ASTRO_ERROR_HEADER]: "true" } });
|
|
80
84
|
}
|
|
85
|
+
const ctx = state.getAPIContext();
|
|
86
|
+
if (this.#pipeline.manifest.checkOrigin && isForbiddenCrossOriginRequest(ctx.request, ctx.url, ctx.isPrerendered)) {
|
|
87
|
+
return createCrossOriginForbiddenResponse(ctx.request);
|
|
88
|
+
}
|
|
81
89
|
try {
|
|
82
|
-
return await this.handle(state,
|
|
90
|
+
return await this.handle(state, ctx);
|
|
83
91
|
} catch (err) {
|
|
84
92
|
app.logger.error(null, err.stack || err.message || String(err));
|
|
85
93
|
return app.renderError(state.request, {
|
|
@@ -11,10 +11,10 @@ function sanitizeParams(params) {
|
|
|
11
11
|
}
|
|
12
12
|
function getParameter(part, params) {
|
|
13
13
|
if (part.spread) {
|
|
14
|
-
return params[part.content.slice(3)]
|
|
14
|
+
return params[part.content.slice(3)] ?? "";
|
|
15
15
|
}
|
|
16
16
|
if (part.dynamic) {
|
|
17
|
-
if (
|
|
17
|
+
if (params[part.content] === void 0) {
|
|
18
18
|
throw new TypeError(`Missing parameter: ${part.content}`);
|
|
19
19
|
}
|
|
20
20
|
return params[part.content];
|
package/dist/core/util.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export declare function resolvePages(config: AstroConfig): URL;
|
|
|
24
24
|
export declare function isPage(file: URL, settings: AstroSettings): boolean;
|
|
25
25
|
export declare function isEndpoint(file: URL, settings: AstroSettings): boolean;
|
|
26
26
|
export declare function resolveJsToTs(filePath: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a path that doesn't name a file on disk (e.g. produced by an
|
|
29
|
+
* extensionless import like `import { Counter } from './Counter'`) to the file
|
|
30
|
+
* Vite would load, by probing Vite's default extension order and directory
|
|
31
|
+
* `index` files. Returns the path unchanged when it already exists as a file
|
|
32
|
+
* or when no candidate is found.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveExtensionlessPath(filePath: string): string;
|
|
27
35
|
/**
|
|
28
36
|
* Set a default NODE_ENV so Vite doesn't set an incorrect default when loading the Astro config
|
|
29
37
|
*/
|
package/dist/core/util.js
CHANGED
|
@@ -94,6 +94,28 @@ function resolveJsToTs(filePath) {
|
|
|
94
94
|
}
|
|
95
95
|
return filePath;
|
|
96
96
|
}
|
|
97
|
+
const VITE_DEFAULT_RESOLVE_EXTENSIONS = [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"];
|
|
98
|
+
function resolveExtensionlessPath(filePath) {
|
|
99
|
+
const stat = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
100
|
+
if (stat?.isFile()) {
|
|
101
|
+
return filePath;
|
|
102
|
+
}
|
|
103
|
+
for (const ext of VITE_DEFAULT_RESOLVE_EXTENSIONS) {
|
|
104
|
+
const tryPath = filePath + ext;
|
|
105
|
+
if (fs.existsSync(tryPath)) {
|
|
106
|
+
return tryPath;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (stat?.isDirectory()) {
|
|
110
|
+
for (const ext of VITE_DEFAULT_RESOLVE_EXTENSIONS) {
|
|
111
|
+
const tryPath = `${filePath}/index${ext}`;
|
|
112
|
+
if (fs.existsSync(tryPath)) {
|
|
113
|
+
return tryPath;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return filePath;
|
|
118
|
+
}
|
|
97
119
|
function ensureProcessNodeEnv(defaultNodeEnv) {
|
|
98
120
|
if (!process.env.NODE_ENV) {
|
|
99
121
|
process.env.NODE_ENV = defaultNodeEnv;
|
|
@@ -106,6 +128,7 @@ export {
|
|
|
106
128
|
isMarkdownFile,
|
|
107
129
|
isPage,
|
|
108
130
|
parseNpmName,
|
|
131
|
+
resolveExtensionlessPath,
|
|
109
132
|
resolveJsToTs,
|
|
110
133
|
resolvePages,
|
|
111
134
|
unwrapId,
|
package/dist/core/viteUtils.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export declare function normalizePath(id: string): string;
|
|
|
9
9
|
* Examples:
|
|
10
10
|
* - `./components/Button.jsx` from `/app/src/pages/index.astro`
|
|
11
11
|
* -> `/app/src/pages/components/Button.tsx` (when `.tsx` exists)
|
|
12
|
+
* - `../components/Counter` from `/app/src/pages/index.astro`
|
|
13
|
+
* -> `/app/src/components/Counter.tsx` (extensionless imports probe Vite's
|
|
14
|
+
* default extension order, then directory `index` files)
|
|
12
15
|
* - `#components/react/Counter.tsx`
|
|
13
16
|
* -> `/app/src/components/react/Counter.tsx` via package `imports`
|
|
14
17
|
*/
|
package/dist/core/viteUtils.js
CHANGED
|
@@ -2,7 +2,13 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { prependForwardSlash, slash } from "../core/path.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
resolveExtensionlessPath,
|
|
7
|
+
resolveJsToTs,
|
|
8
|
+
unwrapId,
|
|
9
|
+
VALID_ID_PREFIX,
|
|
10
|
+
viteID
|
|
11
|
+
} from "./util.js";
|
|
6
12
|
const isWindows = typeof process !== "undefined" && process.platform === "win32";
|
|
7
13
|
function normalizePath(id) {
|
|
8
14
|
return path.posix.normalize(isWindows ? slash(id) : id);
|
|
@@ -10,7 +16,7 @@ function normalizePath(id) {
|
|
|
10
16
|
function resolvePath(specifier, importer) {
|
|
11
17
|
if (specifier.startsWith(".")) {
|
|
12
18
|
const absoluteSpecifier = path.resolve(path.dirname(importer), specifier);
|
|
13
|
-
return resolveJsToTs(normalizePath(absoluteSpecifier));
|
|
19
|
+
return resolveExtensionlessPath(resolveJsToTs(normalizePath(absoluteSpecifier)));
|
|
14
20
|
} else if (specifier.startsWith("#")) {
|
|
15
21
|
try {
|
|
16
22
|
const resolved = createRequire(pathToFileURL(importer)).resolve(specifier);
|
|
@@ -103,7 +103,13 @@ function stringifyChunk(result, chunk) {
|
|
|
103
103
|
out += stringifyChunk(result, instr);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
if (c.chunks.length) {
|
|
107
|
+
for (const part of c.chunks) {
|
|
108
|
+
out += typeof part === "string" ? part : stringifyChunk(result, part);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
out += chunk.toString();
|
|
112
|
+
}
|
|
107
113
|
return out;
|
|
108
114
|
}
|
|
109
115
|
return chunk.toString();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { markHTMLString } from "../escape.js";
|
|
2
2
|
import { renderSlotToString } from "./slot.js";
|
|
3
|
-
import { toAttributeString } from "./util.js";
|
|
3
|
+
import { INVALID_ATTR_NAME_CHAR, toAttributeString } from "./util.js";
|
|
4
4
|
function componentIsHTMLElement(Component) {
|
|
5
5
|
return typeof HTMLElement !== "undefined" && HTMLElement.isPrototypeOf(Component);
|
|
6
6
|
}
|
|
@@ -8,6 +8,9 @@ async function renderHTMLElement(result, constructor, props, slots) {
|
|
|
8
8
|
const name = getHTMLElementName(constructor);
|
|
9
9
|
let attrHTML = "";
|
|
10
10
|
for (const attr in props) {
|
|
11
|
+
if (INVALID_ATTR_NAME_CHAR.test(attr)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
11
14
|
attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
|
|
12
15
|
}
|
|
13
16
|
return markHTMLString(
|
|
@@ -35,3 +35,4 @@ export type TemplateExitInstruction = {
|
|
|
35
35
|
export type RenderInstruction = RenderDirectiveInstruction | RenderHeadInstruction | MaybeRenderHeadInstruction | RendererHydrationScriptInstruction | ServerIslandRuntimeInstruction | RenderScriptInstruction | TemplateEnterInstruction | TemplateExitInstruction;
|
|
36
36
|
export declare function createRenderInstruction<T extends RenderInstruction>(instruction: T): T;
|
|
37
37
|
export declare function isRenderInstruction(chunk: any): chunk is RenderInstruction;
|
|
38
|
+
export declare function isScriptInstruction(chunk: any): chunk is RenderScriptInstruction;
|
|
@@ -7,7 +7,11 @@ function createRenderInstruction(instruction) {
|
|
|
7
7
|
function isRenderInstruction(chunk) {
|
|
8
8
|
return chunk && typeof chunk === "object" && chunk[RenderInstructionSymbol];
|
|
9
9
|
}
|
|
10
|
+
function isScriptInstruction(chunk) {
|
|
11
|
+
return chunk && typeof chunk === "object" && "type" in chunk && chunk.type === "script";
|
|
12
|
+
}
|
|
10
13
|
export {
|
|
11
14
|
createRenderInstruction,
|
|
12
|
-
isRenderInstruction
|
|
15
|
+
isRenderInstruction,
|
|
16
|
+
isScriptInstruction
|
|
13
17
|
};
|
|
@@ -115,14 +115,14 @@ class ServerIslandComponent {
|
|
|
115
115
|
for (const name in this.slots) {
|
|
116
116
|
if (name !== "fallback") {
|
|
117
117
|
const content = await renderSlotToString(this.result, this.slots[name]);
|
|
118
|
-
let slotHtml = content.toString();
|
|
119
118
|
const slotContent = content;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
119
|
+
let slotHtml = "";
|
|
120
|
+
if (slotContent.chunks?.length) {
|
|
121
|
+
for (const part of slotContent.chunks) {
|
|
122
|
+
slotHtml += typeof part === "string" ? part : part.content;
|
|
125
123
|
}
|
|
124
|
+
} else {
|
|
125
|
+
slotHtml = content.toString();
|
|
126
126
|
}
|
|
127
127
|
renderedSlots[name] = slotHtml;
|
|
128
128
|
}
|
|
@@ -2,15 +2,28 @@ import type { SSRResult } from '../../../types/public/internal.js';
|
|
|
2
2
|
import { HTMLString } from '../escape.js';
|
|
3
3
|
import { renderTemplate } from './astro/render-template.js';
|
|
4
4
|
import { type RenderInstance } from './common.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type RenderInstruction, type RenderScriptInstruction } from './instruction.js';
|
|
6
6
|
type RenderTemplateResult = ReturnType<typeof renderTemplate>;
|
|
7
7
|
export type ComponentSlots = Record<string, ComponentSlotValue>;
|
|
8
8
|
export type ComponentSlotValue = (result: SSRResult) => RenderTemplateResult | Promise<RenderTemplateResult>;
|
|
9
9
|
declare const slotString: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* A part of a slot's content stream: either already-stringified HTML or a
|
|
12
|
+
* position-sensitive script instruction that is resolved (and deduplicated)
|
|
13
|
+
* lazily when the slot is finally stringified.
|
|
14
|
+
*/
|
|
15
|
+
export type SlotStringChunk = string | RenderScriptInstruction;
|
|
10
16
|
export declare class SlotString extends HTMLString {
|
|
11
17
|
instructions: null | RenderInstruction[];
|
|
18
|
+
/**
|
|
19
|
+
* The slot's content as an ordered stream. Unlike `instructions` (which holds
|
|
20
|
+
* position-independent instructions like head/hydration content), scripts are
|
|
21
|
+
* kept inline here so they render at their original position instead of being
|
|
22
|
+
* hoisted to the start of the slot output.
|
|
23
|
+
*/
|
|
24
|
+
chunks: SlotStringChunk[];
|
|
12
25
|
[slotString]: boolean;
|
|
13
|
-
constructor(content: string, instructions: null | RenderInstruction[]);
|
|
26
|
+
constructor(content: string, instructions: null | RenderInstruction[], chunks?: SlotStringChunk[]);
|
|
14
27
|
}
|
|
15
28
|
export declare function isSlotString(str: string): str is any;
|
|
16
29
|
/**
|
|
@@ -2,13 +2,24 @@ import { HTMLString, markHTMLString, unescapeHTML } from "../escape.js";
|
|
|
2
2
|
import { renderChild } from "./any.js";
|
|
3
3
|
import { renderTemplate } from "./astro/render-template.js";
|
|
4
4
|
import { chunkToString } from "./common.js";
|
|
5
|
+
import {
|
|
6
|
+
isScriptInstruction
|
|
7
|
+
} from "./instruction.js";
|
|
5
8
|
const slotString = /* @__PURE__ */ Symbol.for("astro:slot-string");
|
|
6
9
|
class SlotString extends HTMLString {
|
|
7
10
|
instructions;
|
|
11
|
+
/**
|
|
12
|
+
* The slot's content as an ordered stream. Unlike `instructions` (which holds
|
|
13
|
+
* position-independent instructions like head/hydration content), scripts are
|
|
14
|
+
* kept inline here so they render at their original position instead of being
|
|
15
|
+
* hoisted to the start of the slot output.
|
|
16
|
+
*/
|
|
17
|
+
chunks;
|
|
8
18
|
[slotString];
|
|
9
|
-
constructor(content, instructions) {
|
|
19
|
+
constructor(content, instructions, chunks = []) {
|
|
10
20
|
super(content);
|
|
11
21
|
this.instructions = instructions;
|
|
22
|
+
this.chunks = chunks;
|
|
12
23
|
this[slotString] = true;
|
|
13
24
|
}
|
|
14
25
|
}
|
|
@@ -35,25 +46,35 @@ function renderSlot(result, slotted, fallback) {
|
|
|
35
46
|
async function renderSlotToString(result, slotted, fallback) {
|
|
36
47
|
let content = "";
|
|
37
48
|
let instructions = null;
|
|
49
|
+
const chunks = [];
|
|
38
50
|
const temporaryDestination = {
|
|
39
51
|
write(chunk) {
|
|
40
52
|
if (chunk instanceof SlotString) {
|
|
41
53
|
content += chunk;
|
|
54
|
+
if (chunk.chunks.length) {
|
|
55
|
+
chunks.push(...chunk.chunks);
|
|
56
|
+
}
|
|
42
57
|
instructions = mergeSlotInstructions(instructions, chunk);
|
|
43
58
|
} else if (chunk instanceof Response) return;
|
|
44
59
|
else if (typeof chunk === "object" && "type" in chunk && typeof chunk.type === "string") {
|
|
45
|
-
if (
|
|
46
|
-
|
|
60
|
+
if (isScriptInstruction(chunk)) {
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
} else {
|
|
63
|
+
if (instructions === null) {
|
|
64
|
+
instructions = [];
|
|
65
|
+
}
|
|
66
|
+
instructions.push(chunk);
|
|
47
67
|
}
|
|
48
|
-
instructions.push(chunk);
|
|
49
68
|
} else {
|
|
50
|
-
|
|
69
|
+
const str = chunkToString(result, chunk);
|
|
70
|
+
content += str;
|
|
71
|
+
chunks.push(str);
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
};
|
|
54
75
|
const renderInstance = renderSlot(result, slotted, fallback);
|
|
55
76
|
await renderInstance.render(temporaryDestination);
|
|
56
|
-
return markHTMLString(new SlotString(content, instructions));
|
|
77
|
+
return markHTMLString(new SlotString(content, instructions, chunks));
|
|
57
78
|
}
|
|
58
79
|
async function renderSlots(result, slots = {}) {
|
|
59
80
|
let slotInstructions = null;
|
|
@@ -68,6 +89,16 @@ async function renderSlots(result, slots = {}) {
|
|
|
68
89
|
}
|
|
69
90
|
slotInstructions.push(...output.instructions);
|
|
70
91
|
}
|
|
92
|
+
if (output.chunks) {
|
|
93
|
+
for (const part of output.chunks) {
|
|
94
|
+
if (typeof part !== "string") {
|
|
95
|
+
if (slotInstructions === null) {
|
|
96
|
+
slotInstructions = [];
|
|
97
|
+
}
|
|
98
|
+
slotInstructions.push(part);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
71
102
|
children[key] = output;
|
|
72
103
|
})
|
|
73
104
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SSRElement } from '../../../types/public/internal.js';
|
|
2
2
|
import type { RenderDestination, RenderFunction } from './common.js';
|
|
3
3
|
export declare const voidElementNames: RegExp;
|
|
4
|
+
export declare const INVALID_ATTR_NAME_CHAR: RegExp;
|
|
4
5
|
export declare const toAttributeString: (value: any, shouldEscape?: boolean) => any;
|
|
5
6
|
export declare const toStyleString: (obj: Record<string, any>) => string;
|
|
6
7
|
export declare function defineScriptVars(vars: Record<any, any>): any;
|
|
@@ -25,6 +25,9 @@ function createVitePluginAstroServer({
|
|
|
25
25
|
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr;
|
|
26
26
|
},
|
|
27
27
|
async configureServer(viteServer) {
|
|
28
|
+
if (process.env.VITEST) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
28
31
|
const ssrEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
|
|
29
32
|
const prerenderEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.prerender];
|
|
30
33
|
const runnableSsrEnvironment = isRunnableDevEnvironment(ssrEnvironment) ? ssrEnvironment : void 0;
|
|
@@ -122,7 +122,7 @@ function astroDevCssPlugin({ routesList, command }) {
|
|
|
122
122
|
}
|
|
123
123
|
for (const collected of collectCSSWithOrder(componentPageId, mod)) {
|
|
124
124
|
if (!cssWithOrder.has(collected.idKey)) {
|
|
125
|
-
const content = cssContentCache.get(collected.
|
|
125
|
+
const content = cssContentCache.get(collected.idKey) || collected.content;
|
|
126
126
|
cssWithOrder.set(collected.idKey, { ...collected, content });
|
|
127
127
|
}
|
|
128
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.6",
|
|
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",
|
|
@@ -146,15 +146,15 @@
|
|
|
146
146
|
"xxhash-wasm": "^1.1.0",
|
|
147
147
|
"yargs-parser": "^22.0.0",
|
|
148
148
|
"zod": "^4.3.6",
|
|
149
|
-
"@astrojs/internal-helpers": "0.10.
|
|
150
|
-
"@astrojs/markdown-satteri": "0.3.
|
|
149
|
+
"@astrojs/internal-helpers": "0.10.1",
|
|
150
|
+
"@astrojs/markdown-satteri": "0.3.3",
|
|
151
151
|
"@astrojs/telemetry": "3.3.2"
|
|
152
152
|
},
|
|
153
153
|
"optionalDependencies": {
|
|
154
154
|
"sharp": "^0.34.0 || ^0.35.0"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@astrojs/markdown-remark": "7.2.
|
|
157
|
+
"@astrojs/markdown-remark": "7.2.1"
|
|
158
158
|
},
|
|
159
159
|
"peerDependenciesMeta": {
|
|
160
160
|
"@astrojs/markdown-remark": {
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"undici": "^7.22.0",
|
|
188
188
|
"vitest": "^4.1.0",
|
|
189
189
|
"@astrojs/check": "0.9.9",
|
|
190
|
-
"@astrojs/markdown-remark": "7.2.
|
|
190
|
+
"@astrojs/markdown-remark": "7.2.1",
|
|
191
191
|
"astro-scripts": "0.0.14"
|
|
192
192
|
},
|
|
193
193
|
"engines": {
|
|
@@ -85,6 +85,10 @@ declare module 'astro:content' {
|
|
|
85
85
|
entry: DataEntryMap[C][string],
|
|
86
86
|
): Promise<RenderResult>;
|
|
87
87
|
|
|
88
|
+
export function render<C extends keyof LiveContentConfig['collections']>(
|
|
89
|
+
entry: import('astro').LiveDataEntry<LiveLoaderDataType<C>>,
|
|
90
|
+
): Promise<RenderResult>;
|
|
91
|
+
|
|
88
92
|
export function reference<
|
|
89
93
|
C extends
|
|
90
94
|
| keyof DataEntryMap
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { MiddlewareHandler } from '../../types/public/common.js';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a middleware function in charge to check the `origin` header.
|
|
4
|
-
*
|
|
5
|
-
* @private
|
|
6
|
-
*/
|
|
7
|
-
export declare function createOriginCheckMiddleware(): MiddlewareHandler;
|
|
8
|
-
export declare function hasFormLikeHeader(contentType: string | null): boolean;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { defineMiddleware } from "../middleware/defineMiddleware.js";
|
|
2
|
-
const FORM_CONTENT_TYPES = [
|
|
3
|
-
"application/x-www-form-urlencoded",
|
|
4
|
-
"multipart/form-data",
|
|
5
|
-
"text/plain"
|
|
6
|
-
];
|
|
7
|
-
const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
8
|
-
function createOriginCheckMiddleware() {
|
|
9
|
-
return defineMiddleware((context, next) => {
|
|
10
|
-
const { request, url, isPrerendered } = context;
|
|
11
|
-
if (isPrerendered) {
|
|
12
|
-
return next();
|
|
13
|
-
}
|
|
14
|
-
if (SAFE_METHODS.includes(request.method)) {
|
|
15
|
-
return next();
|
|
16
|
-
}
|
|
17
|
-
const isSameOrigin = request.headers.get("origin") === url.origin;
|
|
18
|
-
const hasContentType = request.headers.has("content-type");
|
|
19
|
-
if (hasContentType) {
|
|
20
|
-
const formLikeHeader = hasFormLikeHeader(request.headers.get("content-type"));
|
|
21
|
-
if (formLikeHeader && !isSameOrigin) {
|
|
22
|
-
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
23
|
-
status: 403
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
} else {
|
|
27
|
-
if (!isSameOrigin) {
|
|
28
|
-
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
|
|
29
|
-
status: 403
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return next();
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
function hasFormLikeHeader(contentType) {
|
|
37
|
-
if (contentType) {
|
|
38
|
-
for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
|
|
39
|
-
if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
export {
|
|
47
|
-
createOriginCheckMiddleware,
|
|
48
|
-
hasFormLikeHeader
|
|
49
|
-
};
|