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.
Files changed (33) hide show
  1. package/components/Picture.astro +19 -2
  2. package/dist/actions/handler.js +7 -0
  3. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  4. package/dist/cli/install-package.js +5 -3
  5. package/dist/container/index.js +4 -2
  6. package/dist/content/content-layer.js +3 -3
  7. package/dist/core/app/origin-check.d.ts +32 -0
  8. package/dist/core/app/origin-check.js +52 -0
  9. package/dist/core/base-pipeline.js +1 -1
  10. package/dist/core/constants.js +1 -1
  11. package/dist/core/dev/dev.js +1 -1
  12. package/dist/core/messages/runtime.js +1 -1
  13. package/dist/core/pages/handler.js +9 -1
  14. package/dist/core/routing/generator.js +2 -2
  15. package/dist/core/util.d.ts +8 -0
  16. package/dist/core/util.js +23 -0
  17. package/dist/core/viteUtils.d.ts +3 -0
  18. package/dist/core/viteUtils.js +8 -2
  19. package/dist/runtime/server/render/common.js +7 -1
  20. package/dist/runtime/server/render/dom.js +4 -1
  21. package/dist/runtime/server/render/instruction.d.ts +1 -0
  22. package/dist/runtime/server/render/instruction.js +5 -1
  23. package/dist/runtime/server/render/server-islands.js +6 -6
  24. package/dist/runtime/server/render/slot.d.ts +15 -2
  25. package/dist/runtime/server/render/slot.js +37 -6
  26. package/dist/runtime/server/render/util.d.ts +1 -0
  27. package/dist/runtime/server/render/util.js +1 -0
  28. package/dist/vite-plugin-astro-server/plugin.js +3 -0
  29. package/dist/vite-plugin-css/index.js +1 -1
  30. package/package.json +5 -5
  31. package/templates/content/types.d.ts +4 -0
  32. package/dist/core/app/middlewares.d.ts +0 -8
  33. package/dist/core/app/middlewares.js +0 -49
@@ -1,7 +1,13 @@
1
1
  ---
2
- import { getImage, imageConfig, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
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) =>
@@ -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,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.0.5";
3
+ version = "7.0.6";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -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(packageName);
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 packageImport = await import(packageName);
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;
@@ -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(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container");
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(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container");
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.5") {
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.5") {
209
- this.#store.metaStore().set("astro-version", "7.0.5");
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/middlewares.js";
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";
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "7.0.5";
1
+ const ASTRO_VERSION = "7.0.6";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const ASTRO_ERROR_HEADER = "X-Astro-Error";
4
4
  const DEFAULT_404_COMPONENT = "astro-default-404.astro";
@@ -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.5";
29
+ const currentVersion = "7.0.6";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -269,7 +269,7 @@ function printHelp({
269
269
  message.push(
270
270
  linebreak(),
271
271
  ` ${bgGreen(black(` ${commandName} `))} ${green(
272
- `v${"7.0.5"}`
272
+ `v${"7.0.6"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -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, state.getAPIContext());
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 (!params[part.content]) {
17
+ if (params[part.content] === void 0) {
18
18
  throw new TypeError(`Missing parameter: ${part.content}`);
19
19
  }
20
20
  return params[part.content];
@@ -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,
@@ -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
  */
@@ -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 { resolveJsToTs, unwrapId, VALID_ID_PREFIX, viteID } from "./util.js";
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
- out += chunk.toString();
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
- if (Array.isArray(slotContent.instructions)) {
121
- for (const instruction of slotContent.instructions) {
122
- if (instruction.type === "script") {
123
- slotHtml += instruction.content;
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 { RenderInstruction } from './instruction.js';
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 (instructions === null) {
46
- instructions = [];
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
- content += chunkToString(result, chunk);
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;
@@ -168,6 +168,7 @@ function promiseWithResolvers() {
168
168
  };
169
169
  }
170
170
  export {
171
+ INVALID_ATTR_NAME_CHAR,
171
172
  addAttribute,
172
173
  createBufferedRenderer,
173
174
  defineScriptVars,
@@ -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.id) || collected.content;
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.5",
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.0",
150
- "@astrojs/markdown-satteri": "0.3.2",
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.0"
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.0",
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
- };