astro 5.9.4 → 5.10.0

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 (47) hide show
  1. package/client.d.ts +1 -3
  2. package/components/Image.astro +5 -6
  3. package/components/Picture.astro +5 -5
  4. package/components/ResponsivePicture.astro +1 -0
  5. package/dist/actions/integration.d.ts +2 -1
  6. package/dist/actions/integration.js +3 -2
  7. package/dist/actions/utils.d.ts +1 -1
  8. package/dist/actions/utils.js +9 -8
  9. package/dist/assets/internal.d.ts +1 -5
  10. package/dist/assets/internal.js +21 -23
  11. package/dist/assets/types.d.ts +4 -4
  12. package/dist/assets/vite-plugin-assets.js +2 -2
  13. package/dist/content/config.d.ts +74 -0
  14. package/dist/content/config.js +78 -0
  15. package/dist/content/consts.d.ts +1 -0
  16. package/dist/content/consts.js +2 -0
  17. package/dist/content/content-layer.js +3 -3
  18. package/dist/content/loaders/errors.d.ts +20 -0
  19. package/dist/content/loaders/errors.js +64 -0
  20. package/dist/content/loaders/types.d.ts +21 -0
  21. package/dist/content/runtime.d.ts +23 -7
  22. package/dist/content/runtime.js +218 -28
  23. package/dist/content/types-generator.js +11 -4
  24. package/dist/content/utils.d.ts +37 -1
  25. package/dist/content/utils.js +29 -8
  26. package/dist/content/vite-plugin-content-virtual-mod.d.ts +1 -1
  27. package/dist/content/vite-plugin-content-virtual-mod.js +20 -6
  28. package/dist/core/config/schemas/base.d.ts +39 -39
  29. package/dist/core/config/schemas/base.js +8 -8
  30. package/dist/core/config/schemas/refined.js +0 -7
  31. package/dist/core/config/schemas/relative.d.ts +51 -51
  32. package/dist/core/constants.js +1 -1
  33. package/dist/core/csp/config.d.ts +3 -3
  34. package/dist/core/csp/config.js +1 -0
  35. package/dist/core/dev/dev.js +1 -1
  36. package/dist/core/errors/errors-data.d.ts +16 -0
  37. package/dist/core/errors/errors-data.js +15 -4
  38. package/dist/core/errors/errors.js +1 -1
  39. package/dist/core/messages.js +2 -2
  40. package/dist/integrations/hooks.js +5 -2
  41. package/dist/runtime/client/dev-toolbar/apps/astro.js +4 -6
  42. package/dist/types/public/config.d.ts +39 -130
  43. package/dist/types/public/content.d.ts +30 -0
  44. package/package.json +2 -1
  45. package/templates/content/module.mjs +14 -0
  46. package/templates/content/types.d.ts +43 -0
  47. package/types/content.d.ts +23 -80
@@ -3,8 +3,9 @@ import { Traverse } from "neotraverse/modern";
3
3
  import pLimit from "p-limit";
4
4
  import { ZodIssueCode, z } from "zod";
5
5
  import { imageSrcToImportId } from "../assets/utils/resolveImports.js";
6
- import { AstroError, AstroErrorData, AstroUserError } from "../core/errors/index.js";
6
+ import { AstroError, AstroErrorData } from "../core/errors/index.js";
7
7
  import { prependForwardSlash } from "../core/path.js";
8
+ import { defineCollection as defineCollectionOrig } from "./config.js";
8
9
  import {
9
10
  createComponent,
10
11
  createHeadAndContent,
@@ -15,28 +16,14 @@ import {
15
16
  render as serverRender,
16
17
  unescapeHTML
17
18
  } from "../runtime/server/index.js";
18
- import { CONTENT_LAYER_TYPE, IMAGE_IMPORT_PREFIX } from "./consts.js";
19
+ import { IMAGE_IMPORT_PREFIX } from "./consts.js";
19
20
  import { globalDataStore } from "./data-store.js";
20
- function getImporterFilename() {
21
- const stackLine = new Error().stack?.split("\n")?.[3];
22
- if (!stackLine) {
23
- return null;
24
- }
25
- const match = /\/(src\/.*?):\d+:\d+/.exec(stackLine);
26
- return match?.[1] ?? null;
27
- }
28
- function defineCollection(config) {
29
- if ("loader" in config) {
30
- if (config.type && config.type !== CONTENT_LAYER_TYPE) {
31
- throw new AstroUserError(
32
- `Collections that use the Content Layer API must have a \`loader\` defined and no \`type\` set. Check your collection definitions in ${getImporterFilename() ?? "your content config file"}.`
33
- );
34
- }
35
- config.type = CONTENT_LAYER_TYPE;
36
- }
37
- if (!config.type) config.type = "content";
38
- return config;
39
- }
21
+ import {
22
+ LiveCollectionCacheHintError,
23
+ LiveCollectionError,
24
+ LiveCollectionValidationError,
25
+ LiveEntryNotFoundError
26
+ } from "./loaders/errors.js";
40
27
  function createCollectionToGlobResultMap({
41
28
  globResult,
42
29
  contentDir
@@ -52,13 +39,57 @@ function createCollectionToGlobResultMap({
52
39
  }
53
40
  return collectionToGlobResultMap;
54
41
  }
42
+ const cacheHintSchema = z.object({
43
+ tags: z.array(z.string()).optional(),
44
+ maxAge: z.number().optional()
45
+ });
46
+ async function parseLiveEntry(entry, schema, collection) {
47
+ try {
48
+ const parsed = await schema.safeParseAsync(entry.data);
49
+ if (!parsed.success) {
50
+ return {
51
+ error: new LiveCollectionValidationError(collection, entry.id, parsed.error)
52
+ };
53
+ }
54
+ if (entry.cacheHint) {
55
+ const cacheHint = cacheHintSchema.safeParse(entry.cacheHint);
56
+ if (!cacheHint.success) {
57
+ return {
58
+ error: new LiveCollectionCacheHintError(collection, entry.id, cacheHint.error)
59
+ };
60
+ }
61
+ entry.cacheHint = cacheHint.data;
62
+ }
63
+ return {
64
+ entry: {
65
+ ...entry,
66
+ data: parsed.data
67
+ }
68
+ };
69
+ } catch (error) {
70
+ return {
71
+ error: new LiveCollectionError(
72
+ collection,
73
+ `Unexpected error parsing entry ${entry.id} in collection ${collection}`,
74
+ error
75
+ )
76
+ };
77
+ }
78
+ }
55
79
  function createGetCollection({
56
80
  contentCollectionToEntryMap,
57
81
  dataCollectionToEntryMap,
58
82
  getRenderEntryImport,
59
- cacheEntriesByCollection
83
+ cacheEntriesByCollection,
84
+ liveCollections
60
85
  }) {
61
86
  return async function getCollection(collection, filter) {
87
+ if (collection in liveCollections) {
88
+ throw new AstroError({
89
+ ...AstroErrorData.UnknownContentCollectionError,
90
+ message: `Collection "${collection}" is a live collection. Use getLiveCollection() instead of getCollection().`
91
+ });
92
+ }
62
93
  const hasFilter = typeof filter === "function";
63
94
  const store = await globalDataStore.get();
64
95
  let type;
@@ -219,22 +250,35 @@ function emulateLegacyEntry({ legacyId, ...entry }) {
219
250
  function createGetEntry({
220
251
  getEntryImport,
221
252
  getRenderEntryImport,
222
- collectionNames
253
+ collectionNames,
254
+ liveCollections
223
255
  }) {
224
- return async function getEntry(collectionOrLookupObject, _lookupId) {
256
+ return async function getEntry(collectionOrLookupObject, lookup) {
225
257
  let collection, lookupId;
226
258
  if (typeof collectionOrLookupObject === "string") {
227
259
  collection = collectionOrLookupObject;
228
- if (!_lookupId)
260
+ if (!lookup)
229
261
  throw new AstroError({
230
262
  ...AstroErrorData.UnknownContentCollectionError,
231
263
  message: "`getEntry()` requires an entry identifier as the second argument."
232
264
  });
233
- lookupId = _lookupId;
265
+ lookupId = lookup;
234
266
  } else {
235
267
  collection = collectionOrLookupObject.collection;
236
268
  lookupId = "id" in collectionOrLookupObject ? collectionOrLookupObject.id : collectionOrLookupObject.slug;
237
269
  }
270
+ if (collection in liveCollections) {
271
+ throw new AstroError({
272
+ ...AstroErrorData.UnknownContentCollectionError,
273
+ message: `Collection "${collection}" is a live collection. Use getLiveEntry() instead of getEntry().`
274
+ });
275
+ }
276
+ if (typeof lookupId === "object") {
277
+ throw new AstroError({
278
+ ...AstroErrorData.UnknownContentCollectionError,
279
+ message: `The entry identifier must be a string. Received object.`
280
+ });
281
+ }
238
282
  const store = await globalDataStore.get();
239
283
  if (store.hasCollection(collection)) {
240
284
  const entry2 = store.get(collection, lookupId);
@@ -291,6 +335,136 @@ function createGetEntries(getEntry) {
291
335
  return Promise.all(entries.map((e) => getEntry(e)));
292
336
  };
293
337
  }
338
+ function createGetLiveCollection({
339
+ liveCollections
340
+ }) {
341
+ return async function getLiveCollection(collection, filter) {
342
+ if (!(collection in liveCollections)) {
343
+ return {
344
+ error: new LiveCollectionError(
345
+ collection,
346
+ `Collection "${collection}" is not a live collection. Use getCollection() instead of getLiveCollection() to load regular content collections.`
347
+ )
348
+ };
349
+ }
350
+ try {
351
+ const context = {
352
+ filter
353
+ };
354
+ const response = await liveCollections[collection].loader?.loadCollection?.(context);
355
+ if (response && "error" in response) {
356
+ return { error: response.error };
357
+ }
358
+ const { schema } = liveCollections[collection];
359
+ let processedEntries = response.entries;
360
+ if (schema) {
361
+ const entryResults = await Promise.all(
362
+ response.entries.map((entry) => parseLiveEntry(entry, schema, collection))
363
+ );
364
+ for (const result of entryResults) {
365
+ if (result.error) {
366
+ return { error: result.error };
367
+ }
368
+ }
369
+ processedEntries = entryResults.map((result) => result.entry);
370
+ }
371
+ let cacheHint = response.cacheHint;
372
+ if (cacheHint) {
373
+ const cacheHintResult = cacheHintSchema.safeParse(cacheHint);
374
+ if (!cacheHintResult.success) {
375
+ return {
376
+ error: new LiveCollectionCacheHintError(collection, void 0, cacheHintResult.error)
377
+ };
378
+ }
379
+ cacheHint = cacheHintResult.data;
380
+ }
381
+ if (processedEntries.length > 0) {
382
+ const entryTags = /* @__PURE__ */ new Set();
383
+ let minMaxAge;
384
+ for (const entry of processedEntries) {
385
+ if (entry.cacheHint) {
386
+ if (entry.cacheHint.tags) {
387
+ entry.cacheHint.tags.forEach((tag) => entryTags.add(tag));
388
+ }
389
+ if (typeof entry.cacheHint.maxAge === "number") {
390
+ minMaxAge = minMaxAge === void 0 ? entry.cacheHint.maxAge : Math.min(minMaxAge, entry.cacheHint.maxAge);
391
+ }
392
+ }
393
+ }
394
+ if (entryTags.size > 0 || minMaxAge !== void 0 || cacheHint) {
395
+ const mergedCacheHint = {};
396
+ if (cacheHint?.tags || entryTags.size > 0) {
397
+ mergedCacheHint.tags = [...cacheHint?.tags || [], ...entryTags];
398
+ }
399
+ if (cacheHint?.maxAge !== void 0 || minMaxAge !== void 0) {
400
+ mergedCacheHint.maxAge = cacheHint?.maxAge !== void 0 && minMaxAge !== void 0 ? Math.min(cacheHint.maxAge, minMaxAge) : cacheHint?.maxAge ?? minMaxAge;
401
+ }
402
+ cacheHint = mergedCacheHint;
403
+ }
404
+ }
405
+ return {
406
+ entries: processedEntries,
407
+ cacheHint
408
+ };
409
+ } catch (error) {
410
+ return {
411
+ error: new LiveCollectionError(
412
+ collection,
413
+ `Unexpected error loading collection ${collection}`,
414
+ error
415
+ )
416
+ };
417
+ }
418
+ };
419
+ }
420
+ function createGetLiveEntry({
421
+ liveCollections
422
+ }) {
423
+ return async function getLiveEntry(collection, lookup) {
424
+ if (!(collection in liveCollections)) {
425
+ return {
426
+ error: new LiveCollectionError(
427
+ collection,
428
+ `Collection "${collection}" is not a live collection. Use getCollection() instead of getLiveEntry() to load regular content collections.`
429
+ )
430
+ };
431
+ }
432
+ try {
433
+ const lookupObject = {
434
+ filter: typeof lookup === "string" ? { id: lookup } : lookup
435
+ };
436
+ let entry = await liveCollections[collection].loader?.loadEntry?.(lookupObject);
437
+ if (entry && "error" in entry) {
438
+ return { error: entry.error };
439
+ }
440
+ if (!entry) {
441
+ return {
442
+ error: new LiveEntryNotFoundError(collection, lookup)
443
+ };
444
+ }
445
+ const { schema } = liveCollections[collection];
446
+ if (schema) {
447
+ const result = await parseLiveEntry(entry, schema, collection);
448
+ if (result.error) {
449
+ return { error: result.error };
450
+ }
451
+ entry = result.entry;
452
+ }
453
+ return {
454
+ entry,
455
+ cacheHint: entry.cacheHint
456
+ };
457
+ } catch (error) {
458
+ return {
459
+ error: new LiveCollectionError(
460
+ collection,
461
+ `Unexpected error loading entry ${collection} \u2192 ${typeof lookup === "string" ? lookup : JSON.stringify(lookup)}`,
462
+ error
463
+ )
464
+ };
465
+ }
466
+ };
467
+ }
294
468
  const CONTENT_LAYER_IMAGE_REGEX = /__ASTRO_IMAGE_="([^"]+)"/g;
295
469
  async function updateImageReferencesInBody(html, fileName) {
296
470
  const { default: imageAssetMap } = await import("astro:asset-imports");
@@ -499,15 +673,31 @@ function createReference({ lookupMap }) {
499
673
  function isPropagatedAssetsModule(module) {
500
674
  return typeof module === "object" && module != null && "__astroPropagation" in module;
501
675
  }
676
+ function defineCollection(config) {
677
+ if (config.type === "live") {
678
+ throw new AstroError({
679
+ ...AstroErrorData.LiveContentConfigError,
680
+ message: AstroErrorData.LiveContentConfigError.message(
681
+ "Collections with type `live` must be defined in a `src/live.config.ts` file."
682
+ )
683
+ });
684
+ }
685
+ return defineCollectionOrig(config);
686
+ }
502
687
  export {
688
+ LiveCollectionCacheHintError,
689
+ LiveCollectionError,
690
+ LiveCollectionValidationError,
691
+ LiveEntryNotFoundError,
503
692
  createCollectionToGlobResultMap,
504
693
  createGetCollection,
505
694
  createGetDataEntryById,
506
695
  createGetEntries,
507
696
  createGetEntry,
508
697
  createGetEntryBySlug,
698
+ createGetLiveCollection,
699
+ createGetLiveEntry,
509
700
  createReference,
510
701
  defineCollection,
511
- getImporterFilename,
512
702
  renderEntry
513
703
  };
@@ -6,12 +6,13 @@ import { normalizePath } from "vite";
6
6
  import { z } from "zod";
7
7
  import { zodToJsonSchema } from "zod-to-json-schema";
8
8
  import { AstroError } from "../core/errors/errors.js";
9
- import { AstroErrorData } from "../core/errors/index.js";
9
+ import { AstroErrorData, AstroUserError } from "../core/errors/index.js";
10
10
  import { isRelativePath } from "../core/path.js";
11
11
  import {
12
12
  COLLECTIONS_DIR,
13
13
  CONTENT_LAYER_TYPE,
14
14
  CONTENT_TYPES_FILE,
15
+ LIVE_CONTENT_TYPE,
15
16
  VIRTUAL_MODULE_ID
16
17
  } from "./consts.js";
17
18
  import {
@@ -366,6 +367,10 @@ async function writeContentFiles({
366
367
  const collectionEntryKeys = Object.keys(collection.entries).sort();
367
368
  const dataType = await typeForCollection(collectionConfig, collectionKey);
368
369
  switch (resolvedType) {
370
+ case LIVE_CONTENT_TYPE:
371
+ throw new AstroUserError(
372
+ `Invalid definition for collection ${collectionKey}: Live content collections must be defined in "src/live.config.ts"`
373
+ );
369
374
  case "content":
370
375
  if (collectionEntryKeys.length === 0) {
371
376
  contentTypesStr += `${collectionKey}: Record<string, {
@@ -468,16 +473,18 @@ async function writeContentFiles({
468
473
  settings.dotAstroDir.pathname,
469
474
  contentPaths.config.url.pathname
470
475
  );
476
+ const liveConfigPathRelativeToCacheDir = contentPaths.liveConfig?.exists ? normalizeConfigPath(settings.dotAstroDir.pathname, contentPaths.liveConfig.url.pathname) : void 0;
471
477
  for (const contentEntryType of contentEntryTypes) {
472
478
  if (contentEntryType.contentModuleTypes) {
473
479
  typeTemplateContent = contentEntryType.contentModuleTypes + "\n" + typeTemplateContent;
474
480
  }
475
481
  }
476
- typeTemplateContent = typeTemplateContent.replace("// @@CONTENT_ENTRY_MAP@@", contentTypesStr);
477
- typeTemplateContent = typeTemplateContent.replace("// @@DATA_ENTRY_MAP@@", dataTypesStr);
478
- typeTemplateContent = typeTemplateContent.replace(
482
+ typeTemplateContent = typeTemplateContent.replace("// @@CONTENT_ENTRY_MAP@@", contentTypesStr).replace("// @@DATA_ENTRY_MAP@@", dataTypesStr).replace(
479
483
  "'@@CONTENT_CONFIG_TYPE@@'",
480
484
  contentConfig ? `typeof import(${configPathRelativeToCacheDir})` : "never"
485
+ ).replace(
486
+ "'@@LIVE_CONTENT_CONFIG_TYPE@@'",
487
+ liveConfigPathRelativeToCacheDir ? `typeof import(${liveConfigPathRelativeToCacheDir})` : "never"
481
488
  );
482
489
  if (settings.injectedTypes.some((t) => t.filename === CONTENT_TYPES_FILE)) {
483
490
  await fs.promises.writeFile(
@@ -176,6 +176,18 @@ declare const collectionConfigParser: z.ZodUnion<[z.ZodObject<{
176
176
  };
177
177
  schema?: any;
178
178
  _legacy?: boolean | undefined;
179
+ }>, z.ZodObject<{
180
+ type: z.ZodLiteral<"live">;
181
+ schema: z.ZodOptional<z.ZodAny>;
182
+ loader: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>;
183
+ }, "strip", z.ZodTypeAny, {
184
+ type: "live";
185
+ loader: (...args: unknown[]) => unknown;
186
+ schema?: any;
187
+ }, {
188
+ type: "live";
189
+ loader: (...args: unknown[]) => unknown;
190
+ schema?: any;
179
191
  }>]>;
180
192
  declare const contentConfigParser: z.ZodObject<{
181
193
  collections: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodObject<{
@@ -321,6 +333,18 @@ declare const contentConfigParser: z.ZodObject<{
321
333
  };
322
334
  schema?: any;
323
335
  _legacy?: boolean | undefined;
336
+ }>, z.ZodObject<{
337
+ type: z.ZodLiteral<"live">;
338
+ schema: z.ZodOptional<z.ZodAny>;
339
+ loader: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>;
340
+ }, "strip", z.ZodTypeAny, {
341
+ type: "live";
342
+ loader: (...args: unknown[]) => unknown;
343
+ schema?: any;
344
+ }, {
345
+ type: "live";
346
+ loader: (...args: unknown[]) => unknown;
347
+ schema?: any;
324
348
  }>]>>;
325
349
  }, "strip", z.ZodTypeAny, {
326
350
  collections: Record<string, {
@@ -351,6 +375,10 @@ declare const contentConfigParser: z.ZodObject<{
351
375
  };
352
376
  schema?: any;
353
377
  _legacy?: boolean | undefined;
378
+ } | {
379
+ type: "live";
380
+ loader: (...args: unknown[]) => unknown;
381
+ schema?: any;
354
382
  }>;
355
383
  }, {
356
384
  collections: Record<string, {
@@ -381,6 +409,10 @@ declare const contentConfigParser: z.ZodObject<{
381
409
  };
382
410
  schema?: any;
383
411
  _legacy?: boolean | undefined;
412
+ } | {
413
+ type: "live";
414
+ loader: (...args: unknown[]) => unknown;
415
+ schema?: any;
384
416
  }>;
385
417
  }>;
386
418
  export type CollectionConfig = z.infer<typeof collectionConfigParser>;
@@ -484,8 +516,12 @@ export type ContentPaths = {
484
516
  exists: boolean;
485
517
  url: URL;
486
518
  };
519
+ liveConfig: {
520
+ exists: boolean;
521
+ url: URL;
522
+ };
487
523
  };
488
- export declare function getContentPaths({ srcDir, legacy, root }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy'>, fs?: typeof fsMod): ContentPaths;
524
+ export declare function getContentPaths({ srcDir, legacy, root, experimental, }: Pick<AstroConfig, 'root' | 'srcDir' | 'legacy' | 'experimental'>, fs?: typeof fsMod): ContentPaths;
489
525
  /**
490
526
  * Check for slug in content entry frontmatter and validate the type,
491
527
  * falling back to the `generatedSlug` if none is found.
@@ -15,6 +15,7 @@ import {
15
15
  CONTENT_MODULE_FLAG,
16
16
  DEFERRED_MODULE,
17
17
  IMAGE_IMPORT_PREFIX,
18
+ LIVE_CONTENT_TYPE,
18
19
  PROPAGATED_ASSET_FLAG
19
20
  } from "./consts.js";
20
21
  import { glob } from "./loaders/glob.js";
@@ -78,6 +79,11 @@ const collectionConfigParser = z.union([
78
79
  ]),
79
80
  /** deprecated */
80
81
  _legacy: z.boolean().optional()
82
+ }),
83
+ z.object({
84
+ type: z.literal(LIVE_CONTENT_TYPE),
85
+ schema: z.any().optional(),
86
+ loader: z.function()
81
87
  })
82
88
  ]);
83
89
  const contentConfigParser = z.object({
@@ -394,7 +400,7 @@ async function autogenerateCollections({
394
400
  const dataPattern = globWithUnderscoresIgnored("", dataExts);
395
401
  let usesContentLayer = false;
396
402
  for (const collectionName of Object.keys(collections)) {
397
- if (collections[collectionName]?.type === "content_layer") {
403
+ if (collections[collectionName]?.type === "content_layer" || collections[collectionName]?.type === "live") {
398
404
  usesContentLayer = true;
399
405
  continue;
400
406
  }
@@ -496,8 +502,14 @@ function contentObservable(initialCtx) {
496
502
  subscribe
497
503
  };
498
504
  }
499
- function getContentPaths({ srcDir, legacy, root }, fs = fsMod) {
500
- const configStats = search(fs, srcDir, legacy?.collections);
505
+ function getContentPaths({
506
+ srcDir,
507
+ legacy,
508
+ root,
509
+ experimental
510
+ }, fs = fsMod) {
511
+ const configStats = searchConfig(fs, srcDir, legacy?.collections);
512
+ const liveConfigStats = experimental?.liveContentCollections ? searchLiveConfig(fs, srcDir) : { exists: false, url: new URL("./", srcDir) };
501
513
  const pkgBase = new URL("../../", import.meta.url);
502
514
  return {
503
515
  root: new URL("./", root),
@@ -505,23 +517,32 @@ function getContentPaths({ srcDir, legacy, root }, fs = fsMod) {
505
517
  assetsDir: new URL("./assets/", srcDir),
506
518
  typesTemplate: new URL("templates/content/types.d.ts", pkgBase),
507
519
  virtualModTemplate: new URL("templates/content/module.mjs", pkgBase),
508
- config: configStats
520
+ config: configStats,
521
+ liveConfig: liveConfigStats
509
522
  };
510
523
  }
511
- function search(fs, srcDir, legacy) {
524
+ function searchConfig(fs, srcDir, legacy) {
512
525
  const paths = [
513
526
  ...legacy ? [] : ["content.config.mjs", "content.config.js", "content.config.mts", "content.config.ts"],
514
527
  "content/config.mjs",
515
528
  "content/config.js",
516
529
  "content/config.mts",
517
530
  "content/config.ts"
518
- ].map((p) => new URL(`./${p}`, srcDir));
519
- for (const file of paths) {
531
+ ];
532
+ return search(fs, srcDir, paths);
533
+ }
534
+ function searchLiveConfig(fs, srcDir) {
535
+ const paths = ["live.config.mjs", "live.config.js", "live.config.mts", "live.config.ts"];
536
+ return search(fs, srcDir, paths);
537
+ }
538
+ function search(fs, srcDir, paths) {
539
+ const urls = paths.map((p) => new URL(`./${p}`, srcDir));
540
+ for (const file of urls) {
520
541
  if (fs.existsSync(file)) {
521
542
  return { exists: true, url: file };
522
543
  }
523
544
  }
524
- return { exists: false, url: paths[0] };
545
+ return { exists: false, url: urls[0] };
525
546
  }
526
547
  async function getEntrySlug({
527
548
  id,
@@ -1,5 +1,5 @@
1
1
  import nodeFs from 'node:fs';
2
- import type { Plugin } from 'vite';
2
+ import { type Plugin } from 'vite';
3
3
  import type { AstroSettings } from '../types/astro.js';
4
4
  interface AstroContentVirtualModPluginParams {
5
5
  settings: AstroSettings;
@@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { dataToEsm } from "@rollup/pluginutils";
5
5
  import pLimit from "p-limit";
6
6
  import { glob } from "tinyglobby";
7
+ import { normalizePath } from "vite";
7
8
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
8
9
  import { rootRelativePath } from "../core/viteUtils.js";
9
10
  import { createDefaultAstroMetadata } from "../vite-plugin-astro/metadata.js";
@@ -52,11 +53,16 @@ function astroContentVirtualModPlugin({
52
53
  }) {
53
54
  let dataStoreFile;
54
55
  let devServer;
56
+ let liveConfig;
55
57
  return {
56
58
  name: "astro-content-virtual-mod-plugin",
57
59
  enforce: "pre",
58
60
  config(_, env) {
59
61
  dataStoreFile = getDataStoreFile(settings, env.command === "serve");
62
+ const contentPaths = getContentPaths(settings.config);
63
+ if (contentPaths.liveConfig.exists) {
64
+ liveConfig = normalizePath(fileURLToPath(contentPaths.liveConfig.url));
65
+ }
60
66
  },
61
67
  buildStart() {
62
68
  if (devServer) {
@@ -64,8 +70,13 @@ function astroContentVirtualModPlugin({
64
70
  invalidateDataStore(devServer);
65
71
  }
66
72
  },
67
- async resolveId(id) {
73
+ async resolveId(id, importer) {
68
74
  if (id === VIRTUAL_MODULE_ID) {
75
+ if (liveConfig && importer && liveConfig === normalizePath(importer)) {
76
+ return this.resolve("astro/content/config", importer, {
77
+ skipSelf: true
78
+ });
79
+ }
69
80
  return RESOLVED_VIRTUAL_MODULE_ID;
70
81
  }
71
82
  if (id === DATA_STORE_VIRTUAL_ID) {
@@ -202,14 +213,17 @@ async function generateContentEntryFile({
202
213
  message: AstroErrorData.ServerOnlyModule.message("astro:content")
203
214
  });
204
215
  } else {
205
- virtualModContents = nodeFs.readFileSync(contentPaths.virtualModTemplate, "utf-8").replace("@@CONTENT_DIR@@", relContentDir).replace("'@@CONTENT_ENTRY_GLOB_PATH@@'", contentEntryGlobResult).replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult).replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult).replace("/* @@LOOKUP_MAP_ASSIGNMENT@@ */", `lookupMap = ${JSON.stringify(lookupMap)};`);
216
+ virtualModContents = nodeFs.readFileSync(contentPaths.virtualModTemplate, "utf-8").replace("@@CONTENT_DIR@@", relContentDir).replace("'@@CONTENT_ENTRY_GLOB_PATH@@'", contentEntryGlobResult).replace("'@@DATA_ENTRY_GLOB_PATH@@'", dataEntryGlobResult).replace("'@@RENDER_ENTRY_GLOB_PATH@@'", renderEntryGlobResult).replace("/* @@LOOKUP_MAP_ASSIGNMENT@@ */", `lookupMap = ${JSON.stringify(lookupMap)};`).replace(
217
+ "/* @@LIVE_CONTENT_CONFIG@@ */",
218
+ contentPaths.liveConfig.exists ? (
219
+ // Dynamic import so it extracts the chunk and avoids a circular import
220
+ `const liveCollections = (await import(${JSON.stringify(fileURLToPath(contentPaths.liveConfig.url))})).collections;`
221
+ ) : "const liveCollections = {};"
222
+ );
206
223
  }
207
224
  return virtualModContents;
208
225
  }
209
- async function generateLookupMap({
210
- settings,
211
- fs
212
- }) {
226
+ async function generateLookupMap({ settings, fs }) {
213
227
  const { root } = settings.config;
214
228
  const contentPaths = getContentPaths(settings.config);
215
229
  const relContentDir = rootRelativePath(root, contentPaths.contentDir, false);