astro 5.1.7 → 5.1.9

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 (36) hide show
  1. package/astro-jsx.d.ts +37 -12
  2. package/client.d.ts +9 -8
  3. package/dist/actions/integration.js +1 -1
  4. package/dist/content/content-layer.d.ts +3 -1
  5. package/dist/content/content-layer.js +26 -5
  6. package/dist/content/loaders/file.js +1 -0
  7. package/dist/content/loaders/glob.js +1 -0
  8. package/dist/content/runtime.js +1 -13
  9. package/dist/content/server-listeners.js +4 -12
  10. package/dist/content/types-generator.js +7 -1
  11. package/dist/content/utils.d.ts +22 -105
  12. package/dist/content/utils.js +15 -26
  13. package/dist/content/watcher.d.ts +5 -0
  14. package/dist/content/watcher.js +38 -0
  15. package/dist/core/constants.js +1 -1
  16. package/dist/core/dev/container.js +4 -3
  17. package/dist/core/dev/dev.js +1 -1
  18. package/dist/core/dev/restart.js +2 -0
  19. package/dist/core/errors/errors-data.d.ts +20 -0
  20. package/dist/core/errors/errors-data.js +12 -0
  21. package/dist/core/messages.js +2 -2
  22. package/dist/core/routing/3xx.d.ts +1 -1
  23. package/dist/core/routing/3xx.js +1 -1
  24. package/dist/core/routing/manifest/create.js +2 -0
  25. package/dist/core/sync/index.d.ts +3 -1
  26. package/dist/core/sync/index.js +7 -2
  27. package/dist/core/util.js +4 -0
  28. package/dist/env/vite-plugin-import-meta-env.js +3 -1
  29. package/dist/runtime/client/dev-toolbar/apps/audit/rules/a11y.js +2 -2
  30. package/dist/runtime/client/dev-toolbar/apps/audit/rules/perf.js +12 -2
  31. package/dist/runtime/server/render/server-islands.js +5 -1
  32. package/dist/types/public/common.d.ts +1 -1
  33. package/dist/vite-plugin-astro-server/base.js +6 -1
  34. package/dist/vite-plugin-astro-server/response.d.ts +1 -0
  35. package/dist/vite-plugin-astro-server/response.js +12 -0
  36. package/package.json +7 -7
package/astro-jsx.d.ts CHANGED
@@ -21,11 +21,11 @@ declare namespace astroHTML.JSX {
21
21
  children?: Children;
22
22
  }
23
23
 
24
- type AstroComponentDirectives =
25
- import('./dist/types/public/elements.js').AstroComponentDirectives;
24
+ // biome-ignore format: bug
25
+ type AstroComponentDirectives = import('./dist/types/public/elements.js').AstroComponentDirectives;
26
26
  type AstroBuiltinAttributes = import('./dist/types/public/elements.js').AstroBuiltinAttributes;
27
- type AstroDefineVarsAttribute =
28
- import('./dist/types/public/elements.js').AstroDefineVarsAttribute;
27
+ // biome-ignore format: bug
28
+ type AstroDefineVarsAttribute = import('./dist/types/public/elements.js').AstroDefineVarsAttribute;
29
29
  type AstroScriptAttributes = import('./dist/types/public/elements.js').AstroScriptAttributes &
30
30
  AstroDefineVarsAttribute;
31
31
  type AstroStyleAttributes = import('./dist/types/public/elements.js').AstroStyleAttributes &
@@ -174,15 +174,15 @@ declare namespace astroHTML.JSX {
174
174
  onfullscreenerror?: string | undefined | null;
175
175
  }
176
176
 
177
- // All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
177
+ // All the WAI-ARIA 1.2 attributes from https://www.w3.org/TR/wai-aria-1.2/#state_prop_def
178
178
  interface AriaAttributes {
179
- /** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */
179
+ /** Identifies the currently active element when DOM focus is on a composite widget, combobox, textbox, group, or application. */
180
180
  'aria-activedescendant'?: string | undefined | null;
181
181
  /** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */
182
182
  'aria-atomic'?: boolean | 'false' | 'true' | undefined | null;
183
183
  /**
184
- * Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be
185
- * presented if they are made.
184
+ * Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for a combobox, searchbox, or textbox and specifies
185
+ * how predictions would be presented if they were made.
186
186
  */
187
187
  'aria-autocomplete'?: 'none' | 'inline' | 'list' | 'both' | undefined | null;
188
188
  /** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */
@@ -245,11 +245,11 @@ declare namespace astroHTML.JSX {
245
245
  */
246
246
  'aria-dropeffect'?: 'none' | 'copy' | 'execute' | 'link' | 'move' | 'popup' | undefined | null;
247
247
  /**
248
- * Identifies the element that provides an error message for the object.
248
+ * Identifies the element that provides an error message for an object.
249
249
  * @see aria-invalid @see aria-describedby.
250
250
  */
251
251
  'aria-errormessage'?: string | undefined | null;
252
- /** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */
252
+ /** Indicates whether a grouping element owned or controlled by this element is expanded or collapsed. */
253
253
  'aria-expanded'?: boolean | 'false' | 'true' | undefined | null;
254
254
  /**
255
255
  * Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,
@@ -394,32 +394,43 @@ declare namespace astroHTML.JSX {
394
394
  'aria-valuetext'?: string | undefined | null;
395
395
  }
396
396
 
397
- // All the WAI-ARIA 1.1 role attribute values from https://www.w3.org/TR/wai-aria-1.1/#role_definitions
397
+ // All the WAI-ARIA 1.2 role attribute values from https://www.w3.org/TR/wai-aria-1.2/#role_definitions
398
398
  type AriaRole =
399
399
  | 'alert'
400
400
  | 'alertdialog'
401
401
  | 'application'
402
402
  | 'article'
403
403
  | 'banner'
404
+ | 'blockquote'
404
405
  | 'button'
406
+ | 'caption'
405
407
  | 'cell'
406
408
  | 'checkbox'
409
+ | 'code'
407
410
  | 'columnheader'
408
411
  | 'combobox'
412
+ | 'command'
409
413
  | 'complementary'
414
+ | 'composite'
410
415
  | 'contentinfo'
411
416
  | 'definition'
417
+ | 'deletion'
412
418
  | 'dialog'
413
419
  | 'directory'
414
420
  | 'document'
421
+ | 'emphasis'
415
422
  | 'feed'
416
423
  | 'figure'
417
424
  | 'form'
425
+ | 'generic'
418
426
  | 'grid'
419
427
  | 'gridcell'
420
428
  | 'group'
421
429
  | 'heading'
422
430
  | 'img'
431
+ | 'input'
432
+ | 'insertion'
433
+ | 'landmark'
423
434
  | 'link'
424
435
  | 'list'
425
436
  | 'listbox'
@@ -428,6 +439,7 @@ declare namespace astroHTML.JSX {
428
439
  | 'main'
429
440
  | 'marquee'
430
441
  | 'math'
442
+ | 'meter'
431
443
  | 'menu'
432
444
  | 'menubar'
433
445
  | 'menuitem'
@@ -437,21 +449,31 @@ declare namespace astroHTML.JSX {
437
449
  | 'none'
438
450
  | 'note'
439
451
  | 'option'
452
+ | 'paragraph'
440
453
  | 'presentation'
441
454
  | 'progressbar'
442
455
  | 'radio'
443
456
  | 'radiogroup'
457
+ | 'range'
444
458
  | 'region'
459
+ | 'roletype'
445
460
  | 'row'
446
461
  | 'rowgroup'
447
462
  | 'rowheader'
448
463
  | 'scrollbar'
449
464
  | 'search'
450
465
  | 'searchbox'
466
+ | 'section'
467
+ | 'sectionhead'
468
+ | 'select'
451
469
  | 'separator'
452
470
  | 'slider'
453
471
  | 'spinbutton'
454
472
  | 'status'
473
+ | 'strong'
474
+ | 'structure'
475
+ | 'subscript'
476
+ | 'superscript'
455
477
  | 'switch'
456
478
  | 'tab'
457
479
  | 'table'
@@ -459,12 +481,15 @@ declare namespace astroHTML.JSX {
459
481
  | 'tabpanel'
460
482
  | 'term'
461
483
  | 'textbox'
484
+ | 'time'
462
485
  | 'timer'
463
486
  | 'toolbar'
464
487
  | 'tooltip'
465
488
  | 'tree'
466
489
  | 'treegrid'
467
- | 'treeitem';
490
+ | 'treeitem'
491
+ | 'widget'
492
+ | 'window';
468
493
 
469
494
  type CssProperty = keyof Omit<
470
495
  CSSStyleDeclaration,
package/client.d.ts CHANGED
@@ -152,8 +152,8 @@ declare module 'astro:transitions/client' {
152
152
 
153
153
  export type Fallback = import('./dist/virtual-modules/transitions-types.js').Fallback;
154
154
  export type Direction = import('./dist/virtual-modules/transitions-types.ts').Direction;
155
- export type NavigationTypeString =
156
- import('./dist/virtual-modules/transitions-types.js').NavigationTypeString;
155
+ // biome-ignore format: bug
156
+ export type NavigationTypeString = import('./dist/virtual-modules/transitions-types.js').NavigationTypeString;
157
157
  export type Options = import('./dist/virtual-modules/transitions-types.js').Options;
158
158
 
159
159
  type EventModule = typeof import('./dist/virtual-modules/transitions-events.js');
@@ -162,14 +162,15 @@ declare module 'astro:transitions/client' {
162
162
  export const TRANSITION_BEFORE_SWAP: EventModule['TRANSITION_BEFORE_SWAP'];
163
163
  export const TRANSITION_AFTER_SWAP: EventModule['TRANSITION_AFTER_SWAP'];
164
164
  export const TRANSITION_PAGE_LOAD: EventModule['TRANSITION_PAGE_LOAD'];
165
- export type TransitionBeforePreparationEvent =
166
- import('./dist/virtual-modules/transitions-events.js').TransitionBeforePreparationEvent;
167
- export type TransitionBeforeSwapEvent =
168
- import('./dist/virtual-modules/transitions-events.js').TransitionBeforeSwapEvent;
165
+ // biome-ignore format: bug
166
+ export type TransitionBeforePreparationEvent = import('./dist/virtual-modules/transitions-events.js').TransitionBeforePreparationEvent;
167
+ // biome-ignore format: bug
168
+ export type TransitionBeforeSwapEvent = import('./dist/virtual-modules/transitions-events.js').TransitionBeforeSwapEvent;
169
169
  export const isTransitionBeforePreparationEvent: EventModule['isTransitionBeforePreparationEvent'];
170
170
  export const isTransitionBeforeSwapEvent: EventModule['isTransitionBeforeSwapEvent'];
171
- type TransitionSwapFunctionModule =
172
- typeof import('./dist/virtual-modules/transitions-swap-functions.js');
171
+ type TransitionSwapFunctionModule = typeof import(
172
+ './dist/virtual-modules/transitions-swap-functions.js',
173
+ );
173
174
  export const swapFunctions: TransitionSwapFunctionModule['swapFunctions'];
174
175
  }
175
176
 
@@ -27,7 +27,7 @@ function astroIntegrationActionsRouteHandler({
27
27
  throw error;
28
28
  }
29
29
  const stringifiedActionsImport = JSON.stringify(
30
- viteID(new URL("./actions/index.ts", params.config.srcDir))
30
+ viteID(new URL("./actions", params.config.srcDir))
31
31
  );
32
32
  settings.injectedTypes.push({
33
33
  filename: ACTIONS_TYPES_FILE,
@@ -10,6 +10,7 @@ export interface ContentLayerOptions {
10
10
  logger: Logger;
11
11
  watcher?: FSWatcher;
12
12
  }
13
+ type CollectionLoader<TData> = () => Array<TData> | Promise<Array<TData>> | Record<string, Record<string, unknown>> | Promise<Record<string, Record<string, unknown>>>;
13
14
  export declare class ContentLayer {
14
15
  #private;
15
16
  constructor({ settings, logger, store, watcher }: ContentLayerOptions);
@@ -34,7 +35,7 @@ export declare class ContentLayer {
34
35
  }
35
36
  export declare function simpleLoader<TData extends {
36
37
  id: string;
37
- }>(handler: () => Array<TData> | Promise<Array<TData>> | Record<string, Record<string, unknown>> | Promise<Record<string, Record<string, unknown>>>, context: LoaderContext): Promise<void>;
38
+ }>(handler: CollectionLoader<TData>, context: LoaderContext): Promise<void>;
38
39
  /**
39
40
  * Get the path to the data store file.
40
41
  * During development, this is in the `.astro` directory so that the Vite watcher can see it.
@@ -46,3 +47,4 @@ export declare const globalContentLayer: {
46
47
  get: () => ContentLayer | null;
47
48
  dispose: () => void;
48
49
  };
50
+ export {};
@@ -13,8 +13,10 @@ import {
13
13
  getEntryConfigByExtMap,
14
14
  getEntryDataAndImages,
15
15
  globalContentConfigObserver,
16
+ loaderReturnSchema,
16
17
  safeStringify
17
18
  } from "./utils.js";
19
+ import { createWatcherWrapper } from "./watcher.js";
18
20
  class ContentLayer {
19
21
  #logger;
20
22
  #store;
@@ -29,7 +31,9 @@ class ContentLayer {
29
31
  this.#logger = logger;
30
32
  this.#store = store;
31
33
  this.#settings = settings;
32
- this.#watcher = watcher;
34
+ if (watcher) {
35
+ this.#watcher = createWatcherWrapper(watcher);
36
+ }
33
37
  this.#queue = new PQueue({ concurrency: 1 });
34
38
  }
35
39
  /**
@@ -55,6 +59,7 @@ class ContentLayer {
55
59
  dispose() {
56
60
  this.#queue.clear();
57
61
  this.#unsubscribe?.();
62
+ this.#watcher?.removeAllTrackedListeners();
58
63
  }
59
64
  async #getGenerateDigest() {
60
65
  if (this.#generateDigest) {
@@ -148,7 +153,7 @@ ${contentConfig.error.message}`);
148
153
  logger.info("Content config changed");
149
154
  shouldClear = true;
150
155
  }
151
- if (previousAstroVersion && previousAstroVersion !== "5.1.7") {
156
+ if (previousAstroVersion && previousAstroVersion !== "5.1.9") {
152
157
  logger.info("Astro version changed");
153
158
  shouldClear = true;
154
159
  }
@@ -156,8 +161,8 @@ ${contentConfig.error.message}`);
156
161
  logger.info("Clearing content store");
157
162
  this.#store.clearAll();
158
163
  }
159
- if ("5.1.7") {
160
- await this.#store.metaStore().set("astro-version", "5.1.7");
164
+ if ("5.1.9") {
165
+ await this.#store.metaStore().set("astro-version", "5.1.9");
161
166
  }
162
167
  if (currentConfigDigest) {
163
168
  await this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -165,6 +170,9 @@ ${contentConfig.error.message}`);
165
170
  if (astroConfigDigest) {
166
171
  await this.#store.metaStore().set("astro-config-digest", astroConfigDigest);
167
172
  }
173
+ if (!options?.loaders?.length) {
174
+ this.#watcher?.removeAllTrackedListeners();
175
+ }
168
176
  await Promise.all(
169
177
  Object.entries(contentConfig.config.collections).map(async ([name, collection]) => {
170
178
  if (collection.type !== CONTENT_LAYER_TYPE) {
@@ -258,7 +266,20 @@ ${contentConfig.error.message}`);
258
266
  }
259
267
  }
260
268
  async function simpleLoader(handler, context) {
261
- const data = await handler();
269
+ const unsafeData = await handler();
270
+ const parsedData = loaderReturnSchema.safeParse(unsafeData);
271
+ if (!parsedData.success) {
272
+ const issue = parsedData.error.issues[0];
273
+ const parseIssue = Array.isArray(unsafeData) ? issue.unionErrors[0] : issue.unionErrors[1];
274
+ const error = parseIssue.errors[0];
275
+ const firstPathItem = error.path[0];
276
+ const entry = Array.isArray(unsafeData) ? unsafeData[firstPathItem] : unsafeData[firstPathItem];
277
+ throw new AstroError({
278
+ ...AstroErrorData.ContentLoaderReturnsInvalidId,
279
+ message: AstroErrorData.ContentLoaderReturnsInvalidId.message(context.collection, entry)
280
+ });
281
+ }
282
+ const data = parsedData.data;
262
283
  context.store.clear();
263
284
  if (Array.isArray(data)) {
264
285
  for (const raw of data) {
@@ -71,6 +71,7 @@ function file(fileName, options) {
71
71
  }
72
72
  const filePath = fileURLToPath(url);
73
73
  await syncData(filePath, context);
74
+ watcher?.add(filePath);
74
75
  watcher?.on("change", async (changedPath) => {
75
76
  if (changedPath === filePath) {
76
77
  logger.info(`Reloading data from ${fileName}`);
@@ -219,6 +219,7 @@ function glob(globOptions) {
219
219
  if (!watcher) {
220
220
  return;
221
221
  }
222
+ watcher.add(filePath);
222
223
  const matchesGlob = (entry) => !entry.startsWith("../") && micromatch.isMatch(entry, globOptions.pattern);
223
224
  const basePath = fileURLToPath(baseDir);
224
225
  async function onChange(changedPath) {
@@ -468,7 +468,6 @@ This is an Astro bug, so please file an issue at https://github.com/withastro/as
468
468
  return;
469
469
  }
470
470
  const flattenedErrorPath = ctx.path.join(".");
471
- const collectionIsInStore = store.hasCollection(collection);
472
471
  if (typeof lookup === "object") {
473
472
  if (lookup.collection !== collection) {
474
473
  ctx.addIssue({
@@ -479,18 +478,7 @@ This is an Astro bug, so please file an issue at https://github.com/withastro/as
479
478
  }
480
479
  return lookup;
481
480
  }
482
- if (collectionIsInStore) {
483
- const entry2 = store.get(collection, lookup);
484
- if (!entry2) {
485
- ctx.addIssue({
486
- code: ZodIssueCode.custom,
487
- message: `**${flattenedErrorPath}**: Reference to ${collection} invalid. Entry ${lookup} does not exist.`
488
- });
489
- return;
490
- }
491
- return { id: lookup, collection };
492
- }
493
- if (!lookupMap[collection] && store.collections().size <= 1) {
481
+ if (!lookupMap[collection]) {
494
482
  return { id: lookup, collection };
495
483
  }
496
484
  const { type, entries } = lookupMap[collection];
@@ -13,14 +13,7 @@ async function attachContentServerListeners({
13
13
  }) {
14
14
  const contentPaths = getContentPaths(settings.config, fs);
15
15
  if (!settings.config.legacy?.collections) {
16
- const contentGenerator = await createContentTypesGenerator({
17
- fs,
18
- settings,
19
- logger,
20
- viteServer,
21
- contentConfigObserver: globalContentConfigObserver
22
- });
23
- await contentGenerator.init();
16
+ await attachListeners();
24
17
  } else if (fs.existsSync(contentPaths.contentDir)) {
25
18
  logger.debug(
26
19
  "content",
@@ -58,10 +51,9 @@ async function attachContentServerListeners({
58
51
  "addDir",
59
52
  (entry) => contentGenerator.queueEvent({ name: "addDir", entry })
60
53
  );
61
- viteServer.watcher.on(
62
- "change",
63
- (entry) => contentGenerator.queueEvent({ name: "change", entry })
64
- );
54
+ viteServer.watcher.on("change", (entry) => {
55
+ contentGenerator.queueEvent({ name: "change", entry });
56
+ });
65
57
  viteServer.watcher.on("unlink", (entry) => {
66
58
  contentGenerator.queueEvent({ name: "unlink", entry });
67
59
  });
@@ -226,7 +226,13 @@ async function createContentTypesGenerator({
226
226
  entry: pathToFileURL(rawEvent.entry),
227
227
  name: rawEvent.name
228
228
  };
229
- if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
229
+ if (settings.config.legacy.collections) {
230
+ if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) {
231
+ return;
232
+ }
233
+ } else if (contentPaths.config.url.pathname !== event.entry.pathname) {
234
+ return;
235
+ }
230
236
  events.push(event);
231
237
  debounceTimeout && clearTimeout(debounceTimeout);
232
238
  const runEventsSafe = async () => {
@@ -8,7 +8,7 @@ import type { AstroConfig } from '../types/public/config.js';
8
8
  import type { ContentEntryType, DataEntryType } from '../types/public/content.js';
9
9
  import { CONTENT_FLAGS } from './consts.js';
10
10
  /**
11
- * Amap from a collection + slug to the local file path.
11
+ * A map from a collection + slug to the local file path.
12
12
  * This is used internally to resolve entry imports when using `getEntry()`.
13
13
  * @see `templates/content/module.mjs`
14
14
  */
@@ -20,6 +20,19 @@ export type ContentLookupMap = {
20
20
  };
21
21
  };
22
22
  };
23
+ export declare const loaderReturnSchema: z.ZodUnion<[z.ZodArray<z.ZodObject<{
24
+ id: z.ZodString;
25
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
26
+ id: z.ZodString;
27
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
28
+ id: z.ZodString;
29
+ }, z.ZodTypeAny, "passthrough">>, "many">, z.ZodRecord<z.ZodString, z.ZodObject<{
30
+ id: z.ZodOptional<z.ZodString>;
31
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
32
+ id: z.ZodOptional<z.ZodString>;
33
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
34
+ id: z.ZodOptional<z.ZodString>;
35
+ }, z.ZodTypeAny, "passthrough">>>]>;
23
36
  declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
24
37
  type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"content">>>;
25
38
  schema: z.ZodOptional<z.ZodAny>;
@@ -41,31 +54,7 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
41
54
  }>, z.ZodObject<{
42
55
  type: z.ZodLiteral<"content_layer">;
43
56
  schema: z.ZodOptional<z.ZodAny>;
44
- loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnion<[z.ZodArray<z.ZodObject<{
45
- id: z.ZodCatch<z.ZodString>;
46
- }, "strip", z.ZodUnknown, z.objectOutputType<{
47
- id: z.ZodCatch<z.ZodString>;
48
- }, z.ZodUnknown, "strip">, z.objectInputType<{
49
- id: z.ZodCatch<z.ZodString>;
50
- }, z.ZodUnknown, "strip">>, "many">, z.ZodPromise<z.ZodArray<z.ZodObject<{
51
- id: z.ZodCatch<z.ZodString>;
52
- }, "strip", z.ZodUnknown, z.objectOutputType<{
53
- id: z.ZodCatch<z.ZodString>;
54
- }, z.ZodUnknown, "strip">, z.objectInputType<{
55
- id: z.ZodCatch<z.ZodString>;
56
- }, z.ZodUnknown, "strip">>, "many">>, z.ZodRecord<z.ZodString, z.ZodObject<{
57
- id: z.ZodOptional<z.ZodString>;
58
- }, "strip", z.ZodUnknown, z.objectOutputType<{
59
- id: z.ZodOptional<z.ZodString>;
60
- }, z.ZodUnknown, "strip">, z.objectInputType<{
61
- id: z.ZodOptional<z.ZodString>;
62
- }, z.ZodUnknown, "strip">>>, z.ZodPromise<z.ZodRecord<z.ZodString, z.ZodObject<{
63
- id: z.ZodOptional<z.ZodString>;
64
- }, "strip", z.ZodUnknown, z.objectOutputType<{
65
- id: z.ZodOptional<z.ZodString>;
66
- }, z.ZodUnknown, "strip">, z.objectInputType<{
67
- id: z.ZodOptional<z.ZodString>;
68
- }, z.ZodUnknown, "strip">>>>]>>, z.ZodObject<{
57
+ loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>, z.ZodObject<{
69
58
  name: z.ZodString;
70
59
  load: z.ZodFunction<z.ZodTuple<[z.ZodObject<{
71
60
  collection: z.ZodString;
@@ -140,15 +129,7 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
140
129
  _legacy: z.ZodOptional<z.ZodBoolean>;
141
130
  }, "strip", z.ZodTypeAny, {
142
131
  type: "content_layer";
143
- loader: ((...args: unknown[]) => z.objectOutputType<{
144
- id: z.ZodCatch<z.ZodString>;
145
- }, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
146
- id: z.ZodCatch<z.ZodString>;
147
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectOutputType<{
148
- id: z.ZodOptional<z.ZodString>;
149
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectOutputType<{
150
- id: z.ZodOptional<z.ZodString>;
151
- }, z.ZodUnknown, "strip">>>) | {
132
+ loader: ((...args: unknown[]) => unknown) | {
152
133
  load: (args_0: {
153
134
  collection: string;
154
135
  generateDigest: (args_0: any) => unknown;
@@ -169,15 +150,7 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
169
150
  _legacy?: boolean | undefined;
170
151
  }, {
171
152
  type: "content_layer";
172
- loader: ((...args: unknown[]) => z.objectInputType<{
173
- id: z.ZodCatch<z.ZodString>;
174
- }, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
175
- id: z.ZodCatch<z.ZodString>;
176
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectInputType<{
177
- id: z.ZodOptional<z.ZodString>;
178
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectInputType<{
179
- id: z.ZodOptional<z.ZodString>;
180
- }, z.ZodUnknown, "strip">>>) | {
153
+ loader: ((...args: unknown[]) => unknown) | {
181
154
  load: (args_0: {
182
155
  collection: string;
183
156
  generateDigest: (args_0: any) => unknown;
@@ -219,31 +192,7 @@ declare const contentConfigParser: z.ZodObject<{
219
192
  }>, z.ZodObject<{
220
193
  type: z.ZodLiteral<"content_layer">;
221
194
  schema: z.ZodOptional<z.ZodAny>;
222
- loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnion<[z.ZodArray<z.ZodObject<{
223
- id: z.ZodCatch<z.ZodString>;
224
- }, "strip", z.ZodUnknown, z.objectOutputType<{
225
- id: z.ZodCatch<z.ZodString>;
226
- }, z.ZodUnknown, "strip">, z.objectInputType<{
227
- id: z.ZodCatch<z.ZodString>;
228
- }, z.ZodUnknown, "strip">>, "many">, z.ZodPromise<z.ZodArray<z.ZodObject<{
229
- id: z.ZodCatch<z.ZodString>;
230
- }, "strip", z.ZodUnknown, z.objectOutputType<{
231
- id: z.ZodCatch<z.ZodString>;
232
- }, z.ZodUnknown, "strip">, z.objectInputType<{
233
- id: z.ZodCatch<z.ZodString>;
234
- }, z.ZodUnknown, "strip">>, "many">>, z.ZodRecord<z.ZodString, z.ZodObject<{
235
- id: z.ZodOptional<z.ZodString>;
236
- }, "strip", z.ZodUnknown, z.objectOutputType<{
237
- id: z.ZodOptional<z.ZodString>;
238
- }, z.ZodUnknown, "strip">, z.objectInputType<{
239
- id: z.ZodOptional<z.ZodString>;
240
- }, z.ZodUnknown, "strip">>>, z.ZodPromise<z.ZodRecord<z.ZodString, z.ZodObject<{
241
- id: z.ZodOptional<z.ZodString>;
242
- }, "strip", z.ZodUnknown, z.objectOutputType<{
243
- id: z.ZodOptional<z.ZodString>;
244
- }, z.ZodUnknown, "strip">, z.objectInputType<{
245
- id: z.ZodOptional<z.ZodString>;
246
- }, z.ZodUnknown, "strip">>>>]>>, z.ZodObject<{
195
+ loader: z.ZodUnion<[z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>, z.ZodObject<{
247
196
  name: z.ZodString;
248
197
  load: z.ZodFunction<z.ZodTuple<[z.ZodObject<{
249
198
  collection: z.ZodString;
@@ -318,15 +267,7 @@ declare const contentConfigParser: z.ZodObject<{
318
267
  _legacy: z.ZodOptional<z.ZodBoolean>;
319
268
  }, "strip", z.ZodTypeAny, {
320
269
  type: "content_layer";
321
- loader: ((...args: unknown[]) => z.objectOutputType<{
322
- id: z.ZodCatch<z.ZodString>;
323
- }, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
324
- id: z.ZodCatch<z.ZodString>;
325
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectOutputType<{
326
- id: z.ZodOptional<z.ZodString>;
327
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectOutputType<{
328
- id: z.ZodOptional<z.ZodString>;
329
- }, z.ZodUnknown, "strip">>>) | {
270
+ loader: ((...args: unknown[]) => unknown) | {
330
271
  load: (args_0: {
331
272
  collection: string;
332
273
  generateDigest: (args_0: any) => unknown;
@@ -347,15 +288,7 @@ declare const contentConfigParser: z.ZodObject<{
347
288
  _legacy?: boolean | undefined;
348
289
  }, {
349
290
  type: "content_layer";
350
- loader: ((...args: unknown[]) => z.objectInputType<{
351
- id: z.ZodCatch<z.ZodString>;
352
- }, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
353
- id: z.ZodCatch<z.ZodString>;
354
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectInputType<{
355
- id: z.ZodOptional<z.ZodString>;
356
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectInputType<{
357
- id: z.ZodOptional<z.ZodString>;
358
- }, z.ZodUnknown, "strip">>>) | {
291
+ loader: ((...args: unknown[]) => unknown) | {
359
292
  load: (args_0: {
360
293
  collection: string;
361
294
  generateDigest: (args_0: any) => unknown;
@@ -384,15 +317,7 @@ declare const contentConfigParser: z.ZodObject<{
384
317
  schema?: any;
385
318
  } | {
386
319
  type: "content_layer";
387
- loader: ((...args: unknown[]) => z.objectOutputType<{
388
- id: z.ZodCatch<z.ZodString>;
389
- }, z.ZodUnknown, "strip">[] | Promise<z.objectOutputType<{
390
- id: z.ZodCatch<z.ZodString>;
391
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectOutputType<{
392
- id: z.ZodOptional<z.ZodString>;
393
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectOutputType<{
394
- id: z.ZodOptional<z.ZodString>;
395
- }, z.ZodUnknown, "strip">>>) | {
320
+ loader: ((...args: unknown[]) => unknown) | {
396
321
  load: (args_0: {
397
322
  collection: string;
398
323
  generateDigest: (args_0: any) => unknown;
@@ -421,15 +346,7 @@ declare const contentConfigParser: z.ZodObject<{
421
346
  schema?: any;
422
347
  } | {
423
348
  type: "content_layer";
424
- loader: ((...args: unknown[]) => z.objectInputType<{
425
- id: z.ZodCatch<z.ZodString>;
426
- }, z.ZodUnknown, "strip">[] | Promise<z.objectInputType<{
427
- id: z.ZodCatch<z.ZodString>;
428
- }, z.ZodUnknown, "strip">[]> | Record<string, z.objectInputType<{
429
- id: z.ZodOptional<z.ZodString>;
430
- }, z.ZodUnknown, "strip">> | Promise<Record<string, z.objectInputType<{
431
- id: z.ZodOptional<z.ZodString>;
432
- }, z.ZodUnknown, "strip">>>) | {
349
+ loader: ((...args: unknown[]) => unknown) | {
433
350
  load: (args_0: {
434
351
  collection: string;
435
352
  generateDigest: (args_0: any) => unknown;
@@ -24,8 +24,19 @@ const entryTypeSchema = z.object({
24
24
  id: z.string({
25
25
  invalid_type_error: "Content entry `id` must be a string"
26
26
  // Default to empty string so we can validate properly in the loader
27
- }).catch("")
28
- }).catchall(z.unknown());
27
+ })
28
+ }).passthrough();
29
+ const loaderReturnSchema = z.union([
30
+ z.array(entryTypeSchema),
31
+ z.record(
32
+ z.string(),
33
+ z.object({
34
+ id: z.string({
35
+ invalid_type_error: "Content entry `id` must be a string"
36
+ }).optional()
37
+ }).passthrough()
38
+ )
39
+ ]);
29
40
  const collectionConfigParser = z.union([
30
41
  z.object({
31
42
  type: z.literal("content").optional().default("content"),
@@ -39,30 +50,7 @@ const collectionConfigParser = z.union([
39
50
  type: z.literal(CONTENT_LAYER_TYPE),
40
51
  schema: z.any().optional(),
41
52
  loader: z.union([
42
- z.function().returns(
43
- z.union([
44
- z.array(entryTypeSchema),
45
- z.promise(z.array(entryTypeSchema)),
46
- z.record(
47
- z.string(),
48
- z.object({
49
- id: z.string({
50
- invalid_type_error: "Content entry `id` must be a string"
51
- }).optional()
52
- }).catchall(z.unknown())
53
- ),
54
- z.promise(
55
- z.record(
56
- z.string(),
57
- z.object({
58
- id: z.string({
59
- invalid_type_error: "Content entry `id` must be a string"
60
- }).optional()
61
- }).catchall(z.unknown())
62
- )
63
- )
64
- ])
65
- ),
53
+ z.function(),
66
54
  z.object({
67
55
  name: z.string(),
68
56
  load: z.function(
@@ -641,6 +629,7 @@ export {
641
629
  hasAssetPropagationFlag,
642
630
  hasContentFlag,
643
631
  isDeferredModule,
632
+ loaderReturnSchema,
644
633
  parseEntrySlug,
645
634
  posixRelative,
646
635
  posixifyPath,
@@ -0,0 +1,5 @@
1
+ import type { FSWatcher } from 'vite';
2
+ export type WrappedWatcher = FSWatcher & {
3
+ removeAllTrackedListeners(): void;
4
+ };
5
+ export declare function createWatcherWrapper(watcher: FSWatcher): WrappedWatcher;
@@ -0,0 +1,38 @@
1
+ function createWatcherWrapper(watcher) {
2
+ const listeners = /* @__PURE__ */ new Map();
3
+ const handler = {
4
+ get(target, prop, receiver) {
5
+ if (prop === "on") {
6
+ return function(event, callback) {
7
+ if (!listeners.has(event)) {
8
+ listeners.set(event, /* @__PURE__ */ new Set());
9
+ }
10
+ listeners.get(event).add(callback);
11
+ return Reflect.get(target, prop, receiver).call(target, event, callback);
12
+ };
13
+ }
14
+ if (prop === "off") {
15
+ return function(event, callback) {
16
+ listeners.get(event)?.delete(callback);
17
+ return Reflect.get(target, prop, receiver).call(target, event, callback);
18
+ };
19
+ }
20
+ if (prop === "removeAllTrackedListeners") {
21
+ return function() {
22
+ for (const [event, callbacks] of listeners.entries()) {
23
+ for (const callback of callbacks) {
24
+ target.off(event, callback);
25
+ }
26
+ callbacks.clear();
27
+ }
28
+ listeners.clear();
29
+ };
30
+ }
31
+ return Reflect.get(target, prop, receiver);
32
+ }
33
+ };
34
+ return new Proxy(watcher, handler);
35
+ }
36
+ export {
37
+ createWatcherWrapper
38
+ };
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "5.1.7";
1
+ const ASTRO_VERSION = "5.1.9";
2
2
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
3
3
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
4
4
  const REWRITE_DIRECTIVE_HEADER_VALUE = "yes";
@@ -57,19 +57,20 @@ async function createContainer({
57
57
  ssrManifest: devSSRManifest
58
58
  }
59
59
  );
60
+ const viteServer = await vite.createServer(viteConfig);
60
61
  await syncInternal({
61
62
  settings,
62
63
  mode,
63
64
  logger,
64
65
  skip: {
65
- content: true,
66
+ content: !isRestart,
66
67
  cleanup: true
67
68
  },
68
69
  force: inlineConfig?.force,
69
70
  manifest,
70
- command: "dev"
71
+ command: "dev",
72
+ watcher: viteServer.watcher
71
73
  });
72
- const viteServer = await vite.createServer(viteConfig);
73
74
  const container = {
74
75
  inlineConfig: inlineConfig ?? {},
75
76
  fs,
@@ -22,7 +22,7 @@ async function dev(inlineConfig) {
22
22
  await telemetry.record([]);
23
23
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
24
24
  const logger = restart.container.logger;
25
- const currentVersion = "5.1.7";
25
+ const currentVersion = "5.1.9";
26
26
  const isPrerelease = currentVersion.includes("-");
27
27
  if (!isPrerelease) {
28
28
  try {
@@ -1,6 +1,7 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import * as vite from "vite";
3
3
  import { globalContentLayer } from "../../content/content-layer.js";
4
+ import { attachContentServerListeners } from "../../content/server-listeners.js";
4
5
  import { eventCliSession, telemetry } from "../../events/index.js";
5
6
  import { SETTINGS_FILE } from "../../preferences/constants.js";
6
7
  import { createNodeLogger, createSettings, resolveConfig } from "../config/index.js";
@@ -101,6 +102,7 @@ async function createContainerWithAutomaticRestart({
101
102
  } else {
102
103
  restart.container = result;
103
104
  setupContainer();
105
+ await attachContentServerListeners(restart.container);
104
106
  if (server) {
105
107
  server.resolvedUrls = result.viteServer.resolvedUrls;
106
108
  }
@@ -1397,6 +1397,26 @@ export declare const InvalidContentEntryDataError: {
1397
1397
  message(collection: string, entryId: string, error: ZodError): string;
1398
1398
  hint: string;
1399
1399
  };
1400
+ /**
1401
+ * @docs
1402
+ * @message
1403
+ * **Example error message:**<br/>
1404
+ * The content loader for the collection **blog** returned an entry with an invalid `id`:<br/>
1405
+ * &#123;<br/>
1406
+ * "id": 1,<br/>
1407
+ * "title": "Hello, World!"<br/>
1408
+ * &#125;
1409
+ * @description
1410
+ * A content loader returned an invalid `id`.
1411
+ * Make sure that the `id` of the entry is a string.
1412
+ * See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections/) for more information.
1413
+ */
1414
+ export declare const ContentLoaderReturnsInvalidId: {
1415
+ name: string;
1416
+ title: string;
1417
+ message(collection: string, entry: any): string;
1418
+ hint: string;
1419
+ };
1400
1420
  /**
1401
1421
  * @docs
1402
1422
  * @message
@@ -526,6 +526,17 @@ const InvalidContentEntryDataError = {
526
526
  },
527
527
  hint: "See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas."
528
528
  };
529
+ const ContentLoaderReturnsInvalidId = {
530
+ name: "ContentLoaderReturnsInvalidId",
531
+ title: "Content loader returned an entry with an invalid `id`.",
532
+ message(collection, entry) {
533
+ return [
534
+ `The content loader for the collection **${String(collection)}** returned an entry with an invalid \`id\`:`,
535
+ JSON.stringify(entry, null, 2)
536
+ ].join("\n");
537
+ },
538
+ hint: "Make sure that the `id` of the entry is a string. See https://docs.astro.build/en/guides/content-collections/ for more information on content loaders."
539
+ };
529
540
  const ContentEntryDataError = {
530
541
  name: "ContentEntryDataError",
531
542
  title: "Content entry data does not match schema.",
@@ -641,6 +652,7 @@ export {
641
652
  ContentCollectionTypeMismatchError,
642
653
  ContentEntryDataError,
643
654
  ContentLoaderInvalidDataError,
655
+ ContentLoaderReturnsInvalidId,
644
656
  ContentSchemaContainsSlugError,
645
657
  CouldNotTransformImage,
646
658
  DataCollectionEntryParseError,
@@ -38,7 +38,7 @@ function serverStart({
38
38
  host,
39
39
  base
40
40
  }) {
41
- const version = "5.1.7";
41
+ const version = "5.1.9";
42
42
  const localPrefix = `${dim("\u2503")} Local `;
43
43
  const networkPrefix = `${dim("\u2503")} Network `;
44
44
  const emptyPrefix = " ".repeat(11);
@@ -276,7 +276,7 @@ function printHelp({
276
276
  message.push(
277
277
  linebreak(),
278
278
  ` ${bgGreen(black(` ${commandName} `))} ${green(
279
- `v${"5.1.7"}`
279
+ `v${"5.1.9"}`
280
280
  )} ${headline}`
281
281
  );
282
282
  }
@@ -1,5 +1,5 @@
1
1
  export type RedirectTemplate = {
2
- from: string;
2
+ from?: string;
3
3
  location: string | URL;
4
4
  status: number;
5
5
  };
@@ -6,7 +6,7 @@ function redirectTemplate({ status, location, from }) {
6
6
  <meta name="robots" content="noindex">
7
7
  <link rel="canonical" href="${location}">
8
8
  <body>
9
- <a href="${location}">Redirecting from <code>${from}</code> to <code>${location}</code></a>
9
+ <a href="${location}">Redirecting ${from ? `from <code>${from}</code> ` : ""}to <code>${location}</code></a>
10
10
  </body>`;
11
11
  }
12
12
  export {
@@ -483,6 +483,8 @@ async function createRouteManifest(params, logger, { dev = false } = {}) {
483
483
  }
484
484
  if (dev || settings.buildOutput === "server") {
485
485
  injectImageEndpoint(settings, { routes }, dev ? "dev" : "build");
486
+ }
487
+ if (dev || settings.config.adapter) {
486
488
  injectServerIslandRoute(settings.config, { routes });
487
489
  }
488
490
  await runHookRoutesResolved({ routes, settings, logger });
@@ -1,4 +1,5 @@
1
1
  import fsMod from 'node:fs';
2
+ import { type FSWatcher } from 'vite';
2
3
  import type { AstroSettings, ManifestData } from '../../types/astro.js';
3
4
  import type { AstroInlineConfig } from '../../types/public/config.js';
4
5
  import type { Logger } from '../logger/core.js';
@@ -13,6 +14,7 @@ export type SyncOptions = {
13
14
  };
14
15
  manifest: ManifestData;
15
16
  command: 'build' | 'dev' | 'sync';
17
+ watcher?: FSWatcher;
16
18
  };
17
19
  export default function sync(inlineConfig: AstroInlineConfig, { fs, telemetry: _telemetry }?: {
18
20
  fs?: typeof fsMod;
@@ -33,4 +35,4 @@ export declare function clearContentLayerCache({ settings, logger, fs, isDev, }:
33
35
  *
34
36
  * @experimental The JavaScript API is experimental
35
37
  */
36
- export declare function syncInternal({ mode, logger, fs, settings, skip, force, manifest, command, }: SyncOptions): Promise<void>;
38
+ export declare function syncInternal({ mode, logger, fs, settings, skip, force, manifest, command, watcher, }: SyncOptions): Promise<void>;
@@ -74,7 +74,8 @@ async function syncInternal({
74
74
  skip,
75
75
  force,
76
76
  manifest,
77
- command
77
+ command,
78
+ watcher
78
79
  }) {
79
80
  const isDev = command === "dev";
80
81
  if (force) {
@@ -98,8 +99,12 @@ async function syncInternal({
98
99
  const contentLayer = globalContentLayer.init({
99
100
  settings,
100
101
  logger,
101
- store
102
+ store,
103
+ watcher
102
104
  });
105
+ if (watcher) {
106
+ contentLayer.watchContentConfig();
107
+ }
103
108
  await contentLayer.sync();
104
109
  if (!skip?.cleanup) {
105
110
  contentLayer.dispose();
package/dist/core/util.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { hasSpecialQueries } from "../vite-plugin-utils/index.js";
4
5
  import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from "./constants.js";
5
6
  import { removeQueryString, removeTrailingForwardSlash, slash } from "./path.js";
6
7
  function isObject(value) {
@@ -10,6 +11,9 @@ function isURL(value) {
10
11
  return Object.prototype.toString.call(value) === "[object URL]";
11
12
  }
12
13
  function isMarkdownFile(fileId, option) {
14
+ if (hasSpecialQueries(fileId)) {
15
+ return false;
16
+ }
13
17
  const id = removeQueryString(fileId);
14
18
  const _suffix = option?.suffix ?? "";
15
19
  for (let markdownFileExtension of SUPPORTED_MARKDOWN_FILE_EXTENSIONS) {
@@ -1,5 +1,6 @@
1
1
  import { transform } from "esbuild";
2
2
  import MagicString from "magic-string";
3
+ import { createFilter, isCSSRequest } from "vite";
3
4
  const importMetaEnvOnlyRe = /\bimport\.meta\.env\b(?!\.)/;
4
5
  function getReferencedPrivateKeys(source, privateEnv) {
5
6
  const references = /* @__PURE__ */ new Set();
@@ -43,6 +44,7 @@ function importMetaEnv({ envLoader }) {
43
44
  let isDev;
44
45
  let devImportMetaEnvPrepend;
45
46
  let viteConfig;
47
+ const filter = createFilter(null, ["**/*.html", "**/*.htm", "**/*.json"]);
46
48
  return {
47
49
  name: "astro:vite-plugin-env",
48
50
  config(_, { command }) {
@@ -65,7 +67,7 @@ function importMetaEnv({ envLoader }) {
65
67
  }
66
68
  },
67
69
  transform(source, id, options) {
68
- if (!options?.ssr || !source.includes("import.meta.env")) {
70
+ if (!options?.ssr || !source.includes("import.meta.env") || !filter(id) || isCSSRequest(id) || viteConfig.assetsInclude(id)) {
69
71
  return;
70
72
  }
71
73
  privateEnv ??= envLoader.getPrivateEnv();
@@ -176,12 +176,12 @@ const input_type_to_implicit_role = /* @__PURE__ */ new Map([
176
176
  ["url", "textbox"]
177
177
  ]);
178
178
  const ariaAttributes = new Set(
179
- "activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext".split(
179
+ "activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext".split(
180
180
  " "
181
181
  )
182
182
  );
183
183
  const ariaRoles = new Set(
184
- "alert alertdialog application article banner button cell checkbox columnheader combobox complementary contentinfo definition dialog directory document feed figure form grid gridcell group heading img link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status switch tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem".split(
184
+ "alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox command complementary composite contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading img input insertion landmark link list listbox listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option paragraph presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status strong structure subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem widget window".split(
185
185
  " "
186
186
  )
187
187
  );
@@ -23,7 +23,12 @@ const perf = [
23
23
  selector: 'img:not([loading]), img[loading="eager"], iframe:not([loading]), iframe[loading="eager"]',
24
24
  match(element) {
25
25
  const htmlElement = element;
26
- const elementYPosition = htmlElement.getBoundingClientRect().y + window.scrollY;
26
+ let currentElement = element;
27
+ let elementYPosition = 0;
28
+ while (currentElement) {
29
+ elementYPosition += currentElement.offsetTop;
30
+ currentElement = currentElement.offsetParent;
31
+ }
27
32
  if (elementYPosition < window.innerHeight) return false;
28
33
  if (htmlElement.src.startsWith("data:")) return false;
29
34
  return true;
@@ -36,7 +41,12 @@ const perf = [
36
41
  selector: 'img[loading="lazy"], iframe[loading="lazy"]',
37
42
  match(element) {
38
43
  const htmlElement = element;
39
- const elementYPosition = htmlElement.getBoundingClientRect().y + window.scrollY;
44
+ let currentElement = element;
45
+ let elementYPosition = 0;
46
+ while (currentElement) {
47
+ elementYPosition += currentElement.offsetTop;
48
+ currentElement = currentElement.offsetParent;
49
+ }
40
50
  if (elementYPosition > window.innerHeight) return false;
41
51
  if (htmlElement.src.startsWith("data:")) return false;
42
52
  return true;
@@ -10,8 +10,12 @@ const internalProps = /* @__PURE__ */ new Set([
10
10
  function containsServerDirective(props) {
11
11
  return "server:component-directive" in props;
12
12
  }
13
+ const SCRIPT_RE = /<\/script/giu;
14
+ const COMMENT_RE = /<!--/gu;
15
+ const SCRIPT_REPLACER = "<\\/script";
16
+ const COMMENT_REPLACER = "\\u003C!--";
13
17
  function safeJsonStringify(obj) {
14
- return JSON.stringify(obj).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\//g, "\\u002f");
18
+ return JSON.stringify(obj).replace(SCRIPT_RE, SCRIPT_REPLACER).replace(COMMENT_RE, COMMENT_REPLACER);
15
19
  }
16
20
  function createSearchParams(componentExport, encryptedProps, slots) {
17
21
  const params = new URLSearchParams();
@@ -79,7 +79,7 @@ export type PaginateFunction = <PaginateData, AdditionalPaginateProps extends Pr
79
79
  page: Page<PaginateData>;
80
80
  } & OmitIndexSignature<AdditionalPaginateProps>>;
81
81
  }[];
82
- export type APIRoute<Props extends Record<string, any> = Record<string, any>, APIParams extends Record<string, string | undefined> = Record<string, string | undefined>> = (context: APIContext<Props, APIParams>) => Response | Promise<Response>;
82
+ export type APIRoute<APIProps extends Record<string, any> = Record<string, any>, APIParams extends Record<string, string | undefined> = Record<string, string | undefined>> = (context: APIContext<APIProps, APIParams>) => Response | Promise<Response>;
83
83
  export type RewritePayload = string | URL | Request;
84
84
  export type MiddlewareNext = (rewritePayload?: RewritePayload) => Promise<Response>;
85
85
  export type MiddlewareHandler = (context: APIContext, next: MiddlewareNext) => Promise<Response> | Response | Promise<void> | void;
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { appendForwardSlash } from "@astrojs/internal-helpers/path";
4
4
  import { bold } from "kleur/colors";
5
5
  import notFoundTemplate, { subpathNotUsedTemplate } from "../template/4xx.js";
6
- import { writeHtmlResponse } from "./response.js";
6
+ import { writeHtmlResponse, writeRedirectResponse } from "./response.js";
7
+ const manySlashes = /\/{2,}$/;
7
8
  function baseMiddleware(settings, logger) {
8
9
  const { config } = settings;
9
10
  const site = config.site ? new URL(config.base, config.site) : void 0;
@@ -12,6 +13,10 @@ function baseMiddleware(settings, logger) {
12
13
  const devRootReplacement = devRoot.endsWith("/") ? "/" : "";
13
14
  return function devBaseMiddleware(req, res, next) {
14
15
  const url = req.url;
16
+ if (manySlashes.test(url)) {
17
+ const destination = url.replace(manySlashes, "/");
18
+ return writeRedirectResponse(res, 301, destination);
19
+ }
15
20
  let pathname;
16
21
  try {
17
22
  pathname = decodeURI(new URL(url, "http://localhost").pathname);
@@ -4,5 +4,6 @@ import type { ModuleLoader } from '../core/module-loader/index.js';
4
4
  export declare function handle404Response(origin: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
5
5
  export declare function handle500Response(loader: ModuleLoader, res: http.ServerResponse, err: ErrorWithMetadata): Promise<void>;
6
6
  export declare function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string): void;
7
+ export declare function writeRedirectResponse(res: http.ServerResponse, statusCode: number, location: string): void;
7
8
  export declare function writeWebResponse(res: http.ServerResponse, webResponse: Response): Promise<void>;
8
9
  export declare function writeSSRResult(webRequest: Request, webResponse: Response, res: http.ServerResponse): Promise<void>;
@@ -2,6 +2,7 @@ import { Http2ServerResponse } from "node:http2";
2
2
  import { Readable } from "node:stream";
3
3
  import { getSetCookiesFromResponse } from "../core/cookies/index.js";
4
4
  import { getViteErrorPayload } from "../core/errors/dev/index.js";
5
+ import { redirectTemplate } from "../core/routing/3xx.js";
5
6
  import notFoundTemplate from "../template/4xx.js";
6
7
  async function handle404Response(origin, req, res) {
7
8
  const pathname = decodeURI(new URL(origin + req.url).pathname);
@@ -37,6 +38,16 @@ function writeHtmlResponse(res, statusCode, html) {
37
38
  res.write(html);
38
39
  res.end();
39
40
  }
41
+ function writeRedirectResponse(res, statusCode, location) {
42
+ const html = redirectTemplate({ status: statusCode, location });
43
+ res.writeHead(statusCode, {
44
+ Location: location,
45
+ "Content-Type": "text/html",
46
+ "Content-Length": Buffer.byteLength(html, "utf-8")
47
+ });
48
+ res.write(html);
49
+ res.end();
50
+ }
40
51
  async function writeWebResponse(res, webResponse) {
41
52
  const { status, headers, body, statusText } = webResponse;
42
53
  const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
@@ -87,6 +98,7 @@ export {
87
98
  handle404Response,
88
99
  handle500Response,
89
100
  writeHtmlResponse,
101
+ writeRedirectResponse,
90
102
  writeSSRResult,
91
103
  writeWebResponse
92
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "5.1.7",
3
+ "version": "5.1.9",
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",
@@ -143,14 +143,14 @@
143
143
  "prompts": "^2.4.2",
144
144
  "rehype": "^13.0.2",
145
145
  "semver": "^7.6.3",
146
- "shiki": "^1.26.2",
146
+ "shiki": "^1.29.1",
147
147
  "tinyexec": "^0.3.2",
148
148
  "tsconfck": "^3.1.4",
149
149
  "ultrahtml": "^1.5.3",
150
150
  "unist-util-visit": "^5.0.0",
151
151
  "unstorage": "^1.14.4",
152
152
  "vfile": "^6.0.3",
153
- "vite": "^6.0.7",
153
+ "vite": "^6.0.9",
154
154
  "vitefu": "^1.0.5",
155
155
  "which-pm": "^3.0.0",
156
156
  "xxhash-wasm": "^1.1.0",
@@ -196,11 +196,11 @@
196
196
  "rehype-slug": "^6.0.0",
197
197
  "rehype-toc": "^3.0.2",
198
198
  "remark-code-titles": "^0.1.2",
199
- "rollup": "^4.30.1",
200
- "sass": "^1.83.1",
201
- "undici": "^7.2.1",
199
+ "rollup": "^4.31.0",
200
+ "sass": "^1.83.4",
201
+ "undici": "^7.2.3",
202
202
  "unified": "^11.0.5",
203
- "vitest": "^3.0.0-beta.4",
203
+ "vitest": "^3.0.2",
204
204
  "astro-scripts": "0.0.14"
205
205
  },
206
206
  "engines": {