astro 5.0.0-beta.3 → 5.0.0-beta.4

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 (52) hide show
  1. package/client.d.ts +1 -1
  2. package/dist/actions/runtime/virtual/get-action.js +1 -1
  3. package/dist/assets/utils/transformToPath.js +5 -1
  4. package/dist/content/content-layer.js +16 -1
  5. package/dist/content/data-store.d.ts +2 -0
  6. package/dist/content/loaders/glob.d.ts +5 -0
  7. package/dist/content/loaders/glob.js +29 -8
  8. package/dist/content/mutable-data-store.d.ts +2 -19
  9. package/dist/content/mutable-data-store.js +14 -1
  10. package/dist/content/runtime.d.ts +16 -10
  11. package/dist/content/runtime.js +40 -19
  12. package/dist/content/types-generator.js +3 -2
  13. package/dist/content/utils.d.ts +44 -28
  14. package/dist/content/utils.js +96 -3
  15. package/dist/content/vite-plugin-content-virtual-mod.js +24 -31
  16. package/dist/core/app/node.js +2 -1
  17. package/dist/core/app/types.d.ts +1 -1
  18. package/dist/core/base-pipeline.d.ts +2 -2
  19. package/dist/core/base-pipeline.js +4 -1
  20. package/dist/core/build/generate.js +6 -5
  21. package/dist/core/build/static-build.js +2 -2
  22. package/dist/core/config/schema.d.ts +167 -133
  23. package/dist/core/config/schema.js +6 -2
  24. package/dist/core/constants.js +1 -1
  25. package/dist/core/create-vite.js +3 -1
  26. package/dist/core/dev/dev.js +1 -1
  27. package/dist/core/dev/restart.js +5 -2
  28. package/dist/core/errors/dev/utils.js +5 -5
  29. package/dist/core/errors/errors-data.d.ts +12 -2
  30. package/dist/core/errors/errors-data.js +7 -1
  31. package/dist/core/fs/index.d.ts +1 -1
  32. package/dist/core/fs/index.js +2 -5
  33. package/dist/core/logger/vite.js +2 -2
  34. package/dist/core/messages.js +2 -2
  35. package/dist/core/render/params-and-props.js +1 -1
  36. package/dist/core/render-context.js +1 -1
  37. package/dist/runtime/server/astro-island.js +2 -1
  38. package/dist/runtime/server/astro-island.prebuilt-dev.d.ts +1 -1
  39. package/dist/runtime/server/astro-island.prebuilt-dev.js +1 -1
  40. package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
  41. package/dist/runtime/server/astro-island.prebuilt.js +1 -1
  42. package/dist/runtime/server/endpoint.js +11 -2
  43. package/dist/runtime/server/render/server-islands.js +2 -2
  44. package/dist/runtime/server/serialize.js +11 -4
  45. package/dist/types/public/config.d.ts +44 -2
  46. package/dist/vite-plugin-astro/index.js +8 -0
  47. package/dist/vite-plugin-astro-server/response.js +2 -1
  48. package/dist/vite-plugin-hmr-reload/index.d.ts +7 -0
  49. package/dist/vite-plugin-hmr-reload/index.js +28 -0
  50. package/package.json +11 -12
  51. package/templates/actions.mjs +5 -1
  52. package/templates/content/module.mjs +8 -6
package/client.d.ts CHANGED
@@ -19,7 +19,7 @@ interface ImportMeta {
19
19
  * Astro and Vite expose environment variables through `import.meta.env`. For a complete list of the environment variables available, see the two references below.
20
20
  *
21
21
  * - [Astro reference](https://docs.astro.build/en/guides/environment-variables/#default-environment-variables)
22
- * - [Vite reference](https://vitejs.dev/guide/env-and-mode.html#env-variables)
22
+ * - [Vite reference](https://vite.dev/guide/env-and-mode.html#env-variables)
23
23
  */
24
24
  readonly env: ImportMetaEnv;
25
25
  }
@@ -1,7 +1,7 @@
1
1
  import { ActionNotFoundError } from "../../../core/errors/errors-data.js";
2
2
  import { AstroError } from "../../../core/errors/errors.js";
3
3
  async function getAction(path) {
4
- const pathKeys = path.replace("/_actions/", "").split(".");
4
+ const pathKeys = path.replace("/_actions/", "").split(".").map((key) => decodeURIComponent(key));
5
5
  let { server: actionLookup } = await import("astro:internal-actions");
6
6
  if (actionLookup == null || !(typeof actionLookup === "object")) {
7
7
  throw new TypeError(
@@ -6,7 +6,11 @@ import { isESMImportedImage } from "./imageKind.js";
6
6
  function propsToFilename(filePath, transform, hash) {
7
7
  let filename = decodeURIComponent(removeQueryString(filePath));
8
8
  const ext = extname(filename);
9
- filename = basename(filename, ext);
9
+ if (filePath.startsWith("data:")) {
10
+ filename = shorthash(filePath);
11
+ } else {
12
+ filename = basename(filename, ext);
13
+ }
10
14
  const prefixDirname = isESMImportedImage(transform.src) ? dirname(filePath) : "";
11
15
  let outputExt = transform.format ? `.${transform.format}` : ext;
12
16
  return decodeURIComponent(`${prefixDirname}/${filename}_${hash}${outputExt}`);
@@ -107,10 +107,25 @@ class ContentLayer {
107
107
  logger.info("Syncing content");
108
108
  const { digest: currentConfigDigest } = contentConfig.config;
109
109
  this.#lastConfigDigest = currentConfigDigest;
110
+ let shouldClear = false;
110
111
  const previousConfigDigest = await this.#store.metaStore().get("config-digest");
112
+ const previousAstroVersion = await this.#store.metaStore().get("astro-version");
111
113
  if (currentConfigDigest && previousConfigDigest !== currentConfigDigest) {
112
- logger.info("Content config changed, clearing cache");
114
+ logger.info("Content config changed");
115
+ shouldClear = true;
116
+ }
117
+ if (previousAstroVersion !== "5.0.0-beta.4") {
118
+ logger.info("Astro version changed");
119
+ shouldClear = true;
120
+ }
121
+ if (shouldClear) {
122
+ logger.info("Clearing content store");
113
123
  this.#store.clearAll();
124
+ }
125
+ if ("5.0.0-beta.4") {
126
+ await this.#store.metaStore().set("astro-version", "5.0.0-beta.4");
127
+ }
128
+ if (currentConfigDigest) {
114
129
  await this.#store.metaStore().set("config-digest", currentConfigDigest);
115
130
  }
116
131
  await Promise.all(
@@ -31,6 +31,8 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
31
31
  */
32
32
  deferredRender?: boolean;
33
33
  assetImports?: Array<string>;
34
+ /** @deprecated */
35
+ legacyId?: string;
34
36
  }
35
37
  /**
36
38
  * A read-only data store for content collections. This is used to retrieve data from the content layer at runtime.
@@ -23,3 +23,8 @@ export interface GlobOptions {
23
23
  * @param pattern A glob pattern to match files, relative to the content directory.
24
24
  */
25
25
  export declare function glob(globOptions: GlobOptions): Loader;
26
+ /** @private */
27
+ export declare function glob(globOptions: GlobOptions & {
28
+ /** @deprecated */
29
+ _legacy?: true;
30
+ }): Loader;
@@ -9,7 +9,7 @@ function generateIdDefault({ entry, base, data }) {
9
9
  if (data.slug) {
10
10
  return data.slug;
11
11
  }
12
- const entryURL = new URL(entry, base);
12
+ const entryURL = new URL(encodeURI(entry), base);
13
13
  const { slug } = getContentEntryIdAndSlug({
14
14
  entry: entryURL,
15
15
  contentDir: base,
@@ -41,17 +41,19 @@ function glob(globOptions) {
41
41
  load: async ({ config, logger, watcher, parseData, store, generateDigest, entryTypes }) => {
42
42
  const renderFunctionByContentType = /* @__PURE__ */ new WeakMap();
43
43
  const untouchedEntries = new Set(store.keys());
44
+ const isLegacy = globOptions._legacy;
45
+ const emulateLegacyCollections = !config.legacy.collections;
44
46
  async function syncData(entry, base, entryType) {
45
47
  if (!entryType) {
46
48
  logger.warn(`No entry type found for ${entry}`);
47
49
  return;
48
50
  }
49
- const fileUrl = new URL(entry, base);
51
+ const fileUrl = new URL(encodeURI(entry), base);
50
52
  const contents = await fs.readFile(fileUrl, "utf-8").catch((err) => {
51
53
  logger.error(`Error reading ${entry}: ${err.message}`);
52
54
  return;
53
55
  });
54
- if (!contents) {
56
+ if (!contents && contents !== "") {
55
57
  logger.warn(`No contents found for ${entry}`);
56
58
  return;
57
59
  }
@@ -60,6 +62,16 @@ function glob(globOptions) {
60
62
  fileUrl
61
63
  });
62
64
  const id = generateId({ entry, base, data });
65
+ let legacyId;
66
+ if (isLegacy) {
67
+ const entryURL = new URL(encodeURI(entry), base);
68
+ const legacyOptions = getContentEntryIdAndSlug({
69
+ entry: entryURL,
70
+ contentDir: base,
71
+ collection: ""
72
+ });
73
+ legacyId = legacyOptions.id;
74
+ }
63
75
  untouchedEntries.delete(id);
64
76
  const existingEntry = store.get(id);
65
77
  const digest = generateDigest(contents);
@@ -80,6 +92,11 @@ function glob(globOptions) {
80
92
  filePath
81
93
  });
82
94
  if (entryType.getRenderFunction) {
95
+ if (isLegacy && data.layout) {
96
+ logger.error(
97
+ `The Markdown "layout" field is not supported in content collections in Astro 5. Ignoring layout for ${JSON.stringify(entry)}. Enable "legacy.collections" if you need to use the layout field.`
98
+ );
99
+ }
83
100
  let render = renderFunctionByContentType.get(entryType);
84
101
  if (!render) {
85
102
  render = await entryType.getRenderFunction(config);
@@ -104,7 +121,8 @@ function glob(globOptions) {
104
121
  filePath: relativePath,
105
122
  digest,
106
123
  rendered,
107
- assetImports: rendered?.metadata?.imagePaths
124
+ assetImports: rendered?.metadata?.imagePaths,
125
+ legacyId
108
126
  });
109
127
  } else if ("contentModuleTypes" in entryType) {
110
128
  store.set({
@@ -113,10 +131,11 @@ function glob(globOptions) {
113
131
  body,
114
132
  filePath: relativePath,
115
133
  digest,
116
- deferredRender: true
134
+ deferredRender: true,
135
+ legacyId
117
136
  });
118
137
  } else {
119
- store.set({ id, data: parsedData, body, filePath: relativePath, digest });
138
+ store.set({ id, data: parsedData, body, filePath: relativePath, digest, legacyId });
120
139
  }
121
140
  fileToIdMap.set(filePath, id);
122
141
  }
@@ -154,7 +173,7 @@ function glob(globOptions) {
154
173
  if (isConfigFile(entry)) {
155
174
  return;
156
175
  }
157
- if (isInContentDir(entry)) {
176
+ if (!emulateLegacyCollections && isInContentDir(entry)) {
158
177
  skippedFiles.push(entry);
159
178
  return;
160
179
  }
@@ -167,7 +186,9 @@ function glob(globOptions) {
167
186
  const skipCount = skippedFiles.length;
168
187
  if (skipCount > 0) {
169
188
  const patternList = Array.isArray(globOptions.pattern) ? globOptions.pattern.join(", ") : globOptions.pattern;
170
- logger.warn(`The glob() loader cannot be used for files in ${bold("src/content")}.`);
189
+ logger.warn(
190
+ `The glob() loader cannot be used for files in ${bold("src/content")} when legacy mode is enabled.`
191
+ );
171
192
  if (skipCount > 10) {
172
193
  logger.warn(
173
194
  `Skipped ${green(skippedFiles.length)} files that matched ${green(patternList)}.`
@@ -1,5 +1,5 @@
1
1
  import { type PathLike } from 'node:fs';
2
- import { type DataEntry, ImmutableDataStore, type RenderedContent } from './data-store.js';
2
+ import { type DataEntry, ImmutableDataStore } from './data-store.js';
3
3
  /**
4
4
  * Extends the DataStore with the ability to change entries and write them to disk.
5
5
  * This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed.
@@ -34,24 +34,7 @@ export declare class MutableDataStore extends ImmutableDataStore {
34
34
  export interface DataStore {
35
35
  get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) => DataEntry<TData> | undefined;
36
36
  entries: () => Array<[id: string, DataEntry]>;
37
- set: <TData extends Record<string, unknown>>(opts: {
38
- /** The ID of the entry. Must be unique per collection. */
39
- id: string;
40
- /** The data to store. */
41
- data: TData;
42
- /** The raw body of the content, if applicable. */
43
- body?: string;
44
- /** The file path of the content, if applicable. Relative to the site root. */
45
- filePath?: string;
46
- /** A content digest, to check if the content has changed. */
47
- digest?: number | string;
48
- /** The rendered content, if applicable. */
49
- rendered?: RenderedContent;
50
- /**
51
- * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
52
- */
53
- deferredRender?: boolean;
54
- }) => boolean;
37
+ set: <TData extends Record<string, unknown>>(opts: DataEntry<TData>) => boolean;
55
38
  values: () => Array<DataEntry>;
56
39
  keys: () => Array<string>;
57
40
  delete: (key: string) => void;
@@ -159,7 +159,17 @@ ${lines.join(",\n")}]);
159
159
  entries: () => this.entries(collectionName),
160
160
  values: () => this.values(collectionName),
161
161
  keys: () => this.keys(collectionName),
162
- set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
162
+ set: ({
163
+ id: key,
164
+ data,
165
+ body,
166
+ filePath,
167
+ deferredRender,
168
+ digest,
169
+ rendered,
170
+ assetImports,
171
+ legacyId
172
+ }) => {
163
173
  if (!key) {
164
174
  throw new Error(`ID must be a non-empty string`);
165
175
  }
@@ -200,6 +210,9 @@ ${lines.join(",\n")}]);
200
210
  if (rendered) {
201
211
  entry.rendered = rendered;
202
212
  }
213
+ if (legacyId) {
214
+ entry.legacyId = legacyId;
215
+ }
203
216
  if (deferredRender) {
204
217
  entry.deferredRender = deferredRender;
205
218
  if (filePath) {
@@ -18,10 +18,11 @@ export declare function createGetCollection({ contentCollectionToEntryMap, dataC
18
18
  getRenderEntryImport: GetEntryImport;
19
19
  cacheEntriesByCollection: Map<string, any[]>;
20
20
  }): (collection: string, filter?: (entry: any) => unknown) => Promise<any[]>;
21
- export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, collectionNames, }: {
21
+ export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, collectionNames, getEntry, }: {
22
22
  getEntryImport: GetEntryImport;
23
23
  getRenderEntryImport: GetEntryImport;
24
24
  collectionNames: Set<string>;
25
+ getEntry: ReturnType<typeof createGetEntry>;
25
26
  }): (collection: string, slug: string) => Promise<{
26
27
  id: any;
27
28
  slug: any;
@@ -30,10 +31,11 @@ export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImp
30
31
  data: any;
31
32
  render(): Promise<RenderResult>;
32
33
  } | undefined>;
33
- export declare function createGetDataEntryById({ getEntryImport, collectionNames, }: {
34
+ export declare function createGetDataEntryById({ getEntryImport, collectionNames, getEntry, }: {
34
35
  getEntryImport: GetEntryImport;
35
36
  collectionNames: Set<string>;
36
- }): (collection: string, id: string) => Promise<{
37
+ getEntry: ReturnType<typeof createGetEntry>;
38
+ }): (collection: string, id: string) => Promise<ContentEntryResult | {
37
39
  id: any;
38
40
  collection: any;
39
41
  data: any;
@@ -79,7 +81,11 @@ export declare function renderEntry(entry: DataEntry | {
79
81
  render: () => Promise<{
80
82
  Content: AstroComponentFactory;
81
83
  }>;
82
- }): Promise<{
84
+ } | (DataEntry & {
85
+ render: () => Promise<{
86
+ Content: AstroComponentFactory;
87
+ }>;
88
+ })): Promise<{
83
89
  Content: AstroComponentFactory;
84
90
  }>;
85
91
  export declare function createReference({ lookupMap }: {
@@ -88,20 +94,20 @@ export declare function createReference({ lookupMap }: {
88
94
  id: z.ZodString;
89
95
  collection: z.ZodString;
90
96
  }, "strip", z.ZodTypeAny, {
91
- id: string;
92
97
  collection: string;
93
- }, {
94
98
  id: string;
99
+ }, {
95
100
  collection: string;
101
+ id: string;
96
102
  }>, z.ZodObject<{
97
103
  slug: z.ZodString;
98
104
  collection: z.ZodString;
99
105
  }, "strip", z.ZodTypeAny, {
100
- collection: string;
101
106
  slug: string;
102
- }, {
103
107
  collection: string;
108
+ }, {
104
109
  slug: string;
110
+ collection: string;
105
111
  }>]>, {
106
112
  id: string;
107
113
  collection: string;
@@ -109,10 +115,10 @@ export declare function createReference({ lookupMap }: {
109
115
  slug: string;
110
116
  collection: string;
111
117
  } | undefined, string | {
112
- id: string;
113
118
  collection: string;
119
+ id: string;
114
120
  } | {
115
- collection: string;
116
121
  slug: string;
122
+ collection: string;
117
123
  }>;
118
124
  export {};
@@ -71,7 +71,7 @@ function createGetCollection({
71
71
  if (hasFilter && !filter(entry)) {
72
72
  continue;
73
73
  }
74
- result.push(entry);
74
+ result.push(entry.legacyId ? emulateLegacyEntry(entry) : entry);
75
75
  }
76
76
  return result;
77
77
  } else {
@@ -127,18 +127,25 @@ function createGetCollection({
127
127
  function createGetEntryBySlug({
128
128
  getEntryImport,
129
129
  getRenderEntryImport,
130
- collectionNames
130
+ collectionNames,
131
+ getEntry
131
132
  }) {
132
133
  return async function getEntryBySlug(collection, slug) {
133
134
  const store = await globalDataStore.get();
134
135
  if (!collectionNames.has(collection)) {
135
136
  if (store.hasCollection(collection)) {
137
+ const entry2 = await getEntry(collection, slug);
138
+ if (entry2 && "slug" in entry2) {
139
+ return entry2;
140
+ }
136
141
  throw new AstroError({
137
142
  ...AstroErrorData.GetEntryDeprecationError,
138
143
  message: AstroErrorData.GetEntryDeprecationError.message(collection, "getEntryBySlug")
139
144
  });
140
145
  }
141
- console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
146
+ console.warn(
147
+ `The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
148
+ );
142
149
  return void 0;
143
150
  }
144
151
  const entryImport = await getEntryImport(collection, slug);
@@ -162,18 +169,18 @@ function createGetEntryBySlug({
162
169
  }
163
170
  function createGetDataEntryById({
164
171
  getEntryImport,
165
- collectionNames
172
+ collectionNames,
173
+ getEntry
166
174
  }) {
167
175
  return async function getDataEntryById(collection, id) {
168
176
  const store = await globalDataStore.get();
169
177
  if (!collectionNames.has(collection)) {
170
178
  if (store.hasCollection(collection)) {
171
- throw new AstroError({
172
- ...AstroErrorData.GetEntryDeprecationError,
173
- message: AstroErrorData.GetEntryDeprecationError.message(collection, "getDataEntryById")
174
- });
179
+ return getEntry(collection, id);
175
180
  }
176
- console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
181
+ console.warn(
182
+ `The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
183
+ );
177
184
  return void 0;
178
185
  }
179
186
  const lazyImport = await getEntryImport(collection, id);
@@ -186,6 +193,19 @@ function createGetDataEntryById({
186
193
  };
187
194
  };
188
195
  }
196
+ function emulateLegacyEntry(entry) {
197
+ const legacyEntry = {
198
+ ...entry,
199
+ id: entry.legacyId,
200
+ slug: entry.id
201
+ };
202
+ delete legacyEntry.legacyId;
203
+ return {
204
+ ...legacyEntry,
205
+ // Define separately so the render function isn't included in the object passed to `renderEntry()`
206
+ render: () => renderEntry(legacyEntry)
207
+ };
208
+ }
189
209
  function createGetEntry({
190
210
  getEntryImport,
191
211
  getRenderEntryImport,
@@ -214,13 +234,18 @@ function createGetEntry({
214
234
  }
215
235
  const { default: imageAssetMap } = await import("astro:asset-imports");
216
236
  entry2.data = updateImageReferencesInData(entry2.data, entry2.filePath, imageAssetMap);
237
+ if (entry2.legacyId) {
238
+ return { ...emulateLegacyEntry(entry2), collection };
239
+ }
217
240
  return {
218
241
  ...entry2,
219
242
  collection
220
243
  };
221
244
  }
222
245
  if (!collectionNames.has(collection)) {
223
- console.warn(`The collection ${JSON.stringify(collection)} does not exist.`);
246
+ console.warn(
247
+ `The collection ${JSON.stringify(collection)} does not exist. Please ensure it is defined in your content config.`
248
+ );
224
249
  return void 0;
225
250
  }
226
251
  const entryImport = await getEntryImport(collection, lookupId);
@@ -307,7 +332,10 @@ function updateImageReferencesInData(data, fileName, imageAssetMap) {
307
332
  });
308
333
  }
309
334
  async function renderEntry(entry) {
310
- if (entry && "render" in entry) {
335
+ if (!entry) {
336
+ throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
337
+ }
338
+ if ("render" in entry && !("legacyId" in entry)) {
311
339
  return entry.render();
312
340
  }
313
341
  if (entry.deferredRender) {
@@ -431,13 +459,6 @@ function createReference({ lookupMap }) {
431
459
  });
432
460
  return;
433
461
  }
434
- if (!lookupMap[collection] && !collectionIsInStore) {
435
- ctx.addIssue({
436
- code: ZodIssueCode.custom,
437
- message: `**${flattenedErrorPath}:** Reference to ${collection} invalid. Collection does not exist or is empty.`
438
- });
439
- return;
440
- }
441
462
  return lookup;
442
463
  }
443
464
  if (collectionIsInStore) {
@@ -451,7 +472,7 @@ function createReference({ lookupMap }) {
451
472
  }
452
473
  return { id: lookup, collection };
453
474
  }
454
- if (!lookupMap[collection] && store.collections().size === 0) {
475
+ if (!lookupMap[collection] && store.collections().size <= 1) {
455
476
  return { id: lookup, collection };
456
477
  }
457
478
  const { type, entries } = lookupMap[collection];
@@ -390,12 +390,13 @@ async function writeContentFiles({
390
390
  `;
391
391
  break;
392
392
  case CONTENT_LAYER_TYPE:
393
+ const legacyTypes = collectionConfig?._legacy ? 'render(): Render[".md"];\n slug: string;\n body: string;\n' : "body?: string;\n";
393
394
  dataTypesStr += `${collectionKey}: Record<string, {
394
395
  id: string;
395
- collection: ${collectionKey};
396
+ ${legacyTypes} collection: ${collectionKey};
396
397
  data: ${dataType};
397
398
  rendered?: RenderedContent;
398
- filePath?: string
399
+ filePath?: string;
399
400
  }>;
400
401
  `;
401
402
  break;