astro 2.4.3 → 2.4.5

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.
@@ -1,4 +1,5 @@
1
1
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
+ import { joinPaths } from "../../core/path.js";
2
3
  import { VALID_OPTIMIZABLE_FORMATS } from "../consts.js";
3
4
  import { isESMImportedImage } from "../internal.js";
4
5
  function isLocalService(service) {
@@ -87,7 +88,7 @@ const baseService = {
87
88
  options.height && searchParams.append("h", options.height.toString());
88
89
  options.quality && searchParams.append("q", options.quality.toString());
89
90
  options.format && searchParams.append("f", options.format);
90
- return "/_image?" + searchParams;
91
+ return joinPaths(import.meta.env.BASE_URL, "/_image?") + searchParams;
91
92
  },
92
93
  parseURL(url) {
93
94
  const params = url.searchParams;
@@ -6,7 +6,12 @@ import { Readable } from "node:stream";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { normalizePath } from "vite";
8
8
  import { error } from "../core/logger/core.js";
9
- import { appendForwardSlash, joinPaths, prependForwardSlash } from "../core/path.js";
9
+ import {
10
+ appendForwardSlash,
11
+ joinPaths,
12
+ prependForwardSlash,
13
+ removeQueryString
14
+ } from "../core/path.js";
10
15
  import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from "./consts.js";
11
16
  import { isESMImportedImage } from "./internal.js";
12
17
  import { isLocalService } from "./services/service.js";
@@ -184,7 +189,8 @@ function assets({
184
189
  resolvedConfig = viteConfig;
185
190
  },
186
191
  async load(id) {
187
- if (/\.(jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id)) {
192
+ const cleanedUrl = removeQueryString(id);
193
+ if (/\.(jpeg|jpg|png|tiff|webp|gif|svg)$/.test(cleanedUrl)) {
188
194
  const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile, settings);
189
195
  return `export default ${JSON.stringify(meta)}`;
190
196
  }
@@ -1,16 +1,18 @@
1
- type GlobResult = Record<string, () => Promise<any>>;
1
+ type LazyImport = () => Promise<any>;
2
+ type GlobResult = Record<string, LazyImport>;
2
3
  type CollectionToEntryMap = Record<string, GlobResult>;
4
+ type GetEntryImport = (collection: string, lookupId: string) => Promise<LazyImport>;
3
5
  export declare function createCollectionToGlobResultMap({ globResult, contentDir, }: {
4
6
  globResult: GlobResult;
5
7
  contentDir: string;
6
8
  }): CollectionToEntryMap;
7
- export declare function createGetCollection({ collectionToEntryMap, collectionToRenderEntryMap, }: {
9
+ export declare function createGetCollection({ collectionToEntryMap, getRenderEntryImport, }: {
8
10
  collectionToEntryMap: CollectionToEntryMap;
9
- collectionToRenderEntryMap: CollectionToEntryMap;
11
+ getRenderEntryImport: GetEntryImport;
10
12
  }): (collection: string, filter?: ((entry: any) => unknown) | undefined) => Promise<any[]>;
11
- export declare function createGetEntryBySlug({ getCollection, collectionToRenderEntryMap, }: {
12
- getCollection: ReturnType<typeof createGetCollection>;
13
- collectionToRenderEntryMap: CollectionToEntryMap;
13
+ export declare function createGetEntryBySlug({ getEntryImport, getRenderEntryImport, }: {
14
+ getEntryImport: GetEntryImport;
15
+ getRenderEntryImport: GetEntryImport;
14
16
  }): (collection: string, slug: string) => Promise<{
15
17
  id: any;
16
18
  slug: any;
@@ -20,16 +20,15 @@ function createCollectionToGlobResultMap({
20
20
  if (segments.length <= 1)
21
21
  continue;
22
22
  const collection = segments[0];
23
- const entryId = segments.slice(1).join("/");
24
23
  collectionToGlobResultMap[collection] ??= {};
25
- collectionToGlobResultMap[collection][entryId] = globResult[key];
24
+ collectionToGlobResultMap[collection][key] = globResult[key];
26
25
  }
27
26
  return collectionToGlobResultMap;
28
27
  }
29
28
  const cacheEntriesByCollection = /* @__PURE__ */ new Map();
30
29
  function createGetCollection({
31
30
  collectionToEntryMap,
32
- collectionToRenderEntryMap
31
+ getRenderEntryImport
33
32
  }) {
34
33
  return async function getCollection(collection, filter) {
35
34
  const lazyImports = Object.values(collectionToEntryMap[collection] ?? {});
@@ -50,7 +49,7 @@ function createGetCollection({
50
49
  return render({
51
50
  collection: entry.collection,
52
51
  id: entry.id,
53
- collectionToRenderEntryMap
52
+ renderEntryImport: await getRenderEntryImport(collection, entry.slug)
54
53
  });
55
54
  }
56
55
  };
@@ -66,22 +65,14 @@ function createGetCollection({
66
65
  };
67
66
  }
68
67
  function createGetEntryBySlug({
69
- getCollection,
70
- collectionToRenderEntryMap
68
+ getEntryImport,
69
+ getRenderEntryImport
71
70
  }) {
72
71
  return async function getEntryBySlug(collection, slug) {
73
- const entries = await getCollection(collection);
74
- let candidate = void 0;
75
- for (let entry2 of entries) {
76
- if (entry2.slug === slug) {
77
- candidate = entry2;
78
- break;
79
- }
80
- }
81
- if (typeof candidate === "undefined") {
72
+ const entryImport = await getEntryImport(collection, slug);
73
+ if (typeof entryImport !== "function")
82
74
  return void 0;
83
- }
84
- const entry = candidate;
75
+ const entry = await entryImport();
85
76
  return {
86
77
  id: entry.id,
87
78
  slug: entry.slug,
@@ -92,7 +83,7 @@ function createGetEntryBySlug({
92
83
  return render({
93
84
  collection: entry.collection,
94
85
  id: entry.id,
95
- collectionToRenderEntryMap
86
+ renderEntryImport: await getRenderEntryImport(collection, slug)
96
87
  });
97
88
  }
98
89
  };
@@ -101,17 +92,16 @@ function createGetEntryBySlug({
101
92
  async function render({
102
93
  collection,
103
94
  id,
104
- collectionToRenderEntryMap
95
+ renderEntryImport
105
96
  }) {
106
- var _a, _b;
97
+ var _a;
107
98
  const UnexpectedRenderError = new AstroError({
108
99
  ...AstroErrorData.UnknownContentCollectionError,
109
100
  message: `Unexpected error while rendering ${String(collection)} \u2192 ${String(id)}.`
110
101
  });
111
- const lazyImport = (_a = collectionToRenderEntryMap[collection]) == null ? void 0 : _a[id];
112
- if (typeof lazyImport !== "function")
102
+ if (typeof renderEntryImport !== "function")
113
103
  throw UnexpectedRenderError;
114
- const baseMod = await lazyImport();
104
+ const baseMod = await renderEntryImport();
115
105
  if (baseMod == null || typeof baseMod !== "object")
116
106
  throw UnexpectedRenderError;
117
107
  const { collectedStyles, collectedLinks, collectedScripts, getMod } = baseMod;
@@ -158,7 +148,7 @@ async function render({
158
148
  });
159
149
  return {
160
150
  Content,
161
- headings: ((_b = mod.getHeadings) == null ? void 0 : _b.call(mod)) ?? [],
151
+ headings: ((_a = mod.getHeadings) == null ? void 0 : _a.call(mod)) ?? [],
162
152
  remarkPluginFrontmatter: mod.frontmatter ?? {}
163
153
  };
164
154
  }
@@ -6,16 +6,15 @@ import { normalizePath } from "vite";
6
6
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
7
7
  import { info, warn } from "../core/logger/core.js";
8
8
  import { isRelativePath } from "../core/path.js";
9
- import { CONTENT_TYPES_FILE } from "./consts.js";
9
+ import { CONTENT_TYPES_FILE, VIRTUAL_MODULE_ID } from "./consts.js";
10
10
  import {
11
- getContentEntryExts,
11
+ getContentEntryConfigByExtMap,
12
12
  getContentPaths,
13
13
  getEntryInfo,
14
14
  getEntrySlug,
15
15
  getEntryType,
16
16
  loadContentConfig,
17
- NoCollectionError,
18
- parseFrontmatter
17
+ NoCollectionError
19
18
  } from "./utils.js";
20
19
  class UnsupportedFileTypeError extends Error {
21
20
  }
@@ -28,7 +27,8 @@ async function createContentTypesGenerator({
28
27
  }) {
29
28
  const contentTypes = {};
30
29
  const contentPaths = getContentPaths(settings.config, fs);
31
- const contentEntryExts = getContentEntryExts(settings);
30
+ const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
31
+ const contentEntryExts = [...contentEntryConfigByExt.keys()];
32
32
  let events = [];
33
33
  let debounceTimeout;
34
34
  const typeTemplateContent = await fs.promises.readFile(contentPaths.typesTemplate, "utf-8");
@@ -139,12 +139,22 @@ async function createContentTypesGenerator({
139
139
  }
140
140
  return { shouldGenerateTypes: false };
141
141
  }
142
- const { id, collection } = entryInfo;
142
+ const { id, collection, slug: generatedSlug } = entryInfo;
143
+ const contentEntryType = contentEntryConfigByExt.get(path.extname(event.entry.pathname));
144
+ if (!contentEntryType)
145
+ return { shouldGenerateTypes: false };
143
146
  const collectionKey = JSON.stringify(collection);
144
147
  const entryKey = JSON.stringify(id);
145
148
  switch (event.name) {
146
149
  case "add":
147
- const addedSlug = await parseSlug({ fs, event, entryInfo });
150
+ const addedSlug = await getEntrySlug({
151
+ generatedSlug,
152
+ id,
153
+ collection,
154
+ fileUrl: event.entry,
155
+ contentEntryType,
156
+ fs
157
+ });
148
158
  if (!(collectionKey in contentTypes)) {
149
159
  addCollection(contentTypes, collectionKey);
150
160
  }
@@ -158,7 +168,14 @@ async function createContentTypesGenerator({
158
168
  }
159
169
  return { shouldGenerateTypes: true };
160
170
  case "change":
161
- const changedSlug = await parseSlug({ fs, event, entryInfo });
171
+ const changedSlug = await getEntrySlug({
172
+ generatedSlug,
173
+ id,
174
+ collection,
175
+ fileUrl: event.entry,
176
+ contentEntryType,
177
+ fs
178
+ });
162
179
  if (((_b = (_a = contentTypes[collectionKey]) == null ? void 0 : _a[entryKey]) == null ? void 0 : _b.slug) !== changedSlug) {
163
180
  setEntry(contentTypes, collectionKey, entryKey, changedSlug);
164
181
  return { shouldGenerateTypes: true };
@@ -224,6 +241,7 @@ async function createContentTypesGenerator({
224
241
  contentConfig: observable.status === "loaded" ? observable.config : void 0,
225
242
  contentEntryTypes: settings.contentEntryTypes
226
243
  });
244
+ invalidateVirtualMod(viteServer);
227
245
  if (observable.status === "loaded" && ["info", "warn"].includes(logLevel)) {
228
246
  warnNonexistentCollections({
229
247
  logging,
@@ -235,21 +253,18 @@ async function createContentTypesGenerator({
235
253
  }
236
254
  return { init, queueEvent };
237
255
  }
256
+ function invalidateVirtualMod(viteServer) {
257
+ const virtualMod = viteServer.moduleGraph.getModuleById("\0" + VIRTUAL_MODULE_ID);
258
+ if (!virtualMod)
259
+ return;
260
+ viteServer.moduleGraph.invalidateModule(virtualMod);
261
+ }
238
262
  function addCollection(contentMap, collectionKey) {
239
263
  contentMap[collectionKey] = {};
240
264
  }
241
265
  function removeCollection(contentMap, collectionKey) {
242
266
  delete contentMap[collectionKey];
243
267
  }
244
- async function parseSlug({
245
- fs,
246
- event,
247
- entryInfo
248
- }) {
249
- const rawContents = await fs.promises.readFile(event.entry, "utf-8");
250
- const { data: frontmatter } = parseFrontmatter(rawContents, fileURLToPath(event.entry));
251
- return getEntrySlug({ ...entryInfo, unvalidatedSlug: frontmatter.slug });
252
- }
253
268
  function setEntry(contentTypes, collectionKey, entryKey, slug) {
254
269
  contentTypes[collectionKey][entryKey] = { slug };
255
270
  }
@@ -4,7 +4,7 @@ import fsMod from 'node:fs';
4
4
  import type { PluginContext } from 'rollup';
5
5
  import { type ViteDevServer } from 'vite';
6
6
  import { z } from 'zod';
7
- import type { AstroConfig, AstroSettings } from '../@types/astro.js';
7
+ import type { AstroConfig, AstroSettings, ContentEntryType } from '../@types/astro.js';
8
8
  export declare const collectionConfigParser: z.ZodObject<{
9
9
  schema: z.ZodOptional<z.ZodAny>;
10
10
  }, "strip", z.ZodTypeAny, {
@@ -47,8 +47,11 @@ export type EntryInfo = {
47
47
  export declare const msg: {
48
48
  collectionConfigMissing: (collection: string) => string;
49
49
  };
50
- export declare function getEntrySlug({ id, collection, slug, unvalidatedSlug, }: EntryInfo & {
51
- unvalidatedSlug?: unknown;
50
+ export declare function parseEntrySlug({ id, collection, generatedSlug, frontmatterSlug, }: {
51
+ id: string;
52
+ collection: string;
53
+ generatedSlug: string;
54
+ frontmatterSlug?: unknown;
52
55
  }): string;
53
56
  export declare function getEntryData(entry: EntryInfo & {
54
57
  unvalidatedData: Record<string, unknown>;
@@ -57,13 +60,15 @@ export declare function getEntryData(entry: EntryInfo & {
57
60
  [x: string]: unknown;
58
61
  }>;
59
62
  export declare function getContentEntryExts(settings: Pick<AstroSettings, 'contentEntryTypes'>): string[];
63
+ export declare function getContentEntryConfigByExtMap(settings: Pick<AstroSettings, 'contentEntryTypes'>): Map<string, ContentEntryType>;
60
64
  export declare class NoCollectionError extends Error {
61
65
  }
62
66
  export declare function getEntryInfo(params: Pick<ContentPaths, 'contentDir'> & {
63
- entry: URL;
67
+ entry: string | URL;
64
68
  allowFilesOutsideCollection?: true;
65
69
  }): EntryInfo;
66
70
  export declare function getEntryType(entryPath: string, paths: Pick<ContentPaths, 'config' | 'contentDir'>, contentFileExts: string[], experimentalAssets: boolean): 'content' | 'config' | 'ignored' | 'unsupported';
71
+ export declare function hasUnderscoreBelowContentDirectoryPath(fileUrl: URL, contentDir: ContentPaths['contentDir']): boolean;
67
72
  /**
68
73
  * Match YAML exception handling from Astro core errors
69
74
  * @see 'astro/src/core/errors.ts'
@@ -113,4 +118,17 @@ export type ContentPaths = {
113
118
  };
114
119
  };
115
120
  export declare function getContentPaths({ srcDir, root }: Pick<AstroConfig, 'root' | 'srcDir'>, fs?: typeof fsMod): ContentPaths;
121
+ /**
122
+ * Check for slug in content entry frontmatter and validate the type,
123
+ * falling back to the `generatedSlug` if none is found.
124
+ */
125
+ export declare function getEntrySlug({ id, collection, generatedSlug, contentEntryType, fileUrl, fs, }: {
126
+ fs: typeof fsMod;
127
+ id: string;
128
+ collection: string;
129
+ generatedSlug: string;
130
+ fileUrl: URL;
131
+ contentEntryType: Pick<ContentEntryType, 'getEntryInfo'>;
132
+ }): Promise<string>;
133
+ export declare function getExtGlob(exts: string[]): string;
116
134
  export {};
@@ -26,14 +26,14 @@ const contentConfigParser = z.object({
26
26
  const msg = {
27
27
  collectionConfigMissing: (collection) => `${collection} does not have a config. We suggest adding one for type safety!`
28
28
  };
29
- function getEntrySlug({
29
+ function parseEntrySlug({
30
30
  id,
31
31
  collection,
32
- slug,
33
- unvalidatedSlug
32
+ generatedSlug,
33
+ frontmatterSlug
34
34
  }) {
35
35
  try {
36
- return z.string().default(slug).parse(unvalidatedSlug);
36
+ return z.string().default(generatedSlug).parse(frontmatterSlug);
37
37
  } catch {
38
38
  throw new AstroError({
39
39
  ...AstroErrorData.InvalidContentEntrySlugError,
@@ -91,6 +91,15 @@ async function getEntryData(entry, collectionConfig, pluginContext, settings) {
91
91
  function getContentEntryExts(settings) {
92
92
  return settings.contentEntryTypes.map((t) => t.extensions).flat();
93
93
  }
94
+ function getContentEntryConfigByExtMap(settings) {
95
+ const map = /* @__PURE__ */ new Map();
96
+ for (const entryType of settings.contentEntryTypes) {
97
+ for (const ext of entryType.extensions) {
98
+ map.set(ext, entryType);
99
+ }
100
+ }
101
+ return map;
102
+ }
94
103
  class NoCollectionError extends Error {
95
104
  }
96
105
  function getEntryInfo({
@@ -98,7 +107,10 @@ function getEntryInfo({
98
107
  contentDir,
99
108
  allowFilesOutsideCollection = false
100
109
  }) {
101
- const rawRelativePath = path.relative(fileURLToPath(contentDir), fileURLToPath(entry));
110
+ const rawRelativePath = path.relative(
111
+ fileURLToPath(contentDir),
112
+ typeof entry === "string" ? entry : fileURLToPath(entry)
113
+ );
102
114
  const rawCollection = path.dirname(rawRelativePath).split(path.sep).shift();
103
115
  const isOutsideCollection = rawCollection === ".." || rawCollection === ".";
104
116
  if (!rawCollection || !allowFilesOutsideCollection && isOutsideCollection)
@@ -238,11 +250,38 @@ function search(fs, srcDir) {
238
250
  }
239
251
  return { exists: false, url: paths[0] };
240
252
  }
253
+ async function getEntrySlug({
254
+ id,
255
+ collection,
256
+ generatedSlug,
257
+ contentEntryType,
258
+ fileUrl,
259
+ fs
260
+ }) {
261
+ let contents;
262
+ try {
263
+ contents = await fs.promises.readFile(fileUrl, "utf-8");
264
+ } catch (e) {
265
+ throw new AstroError(AstroErrorData.UnknownContentCollectionError, { cause: e });
266
+ }
267
+ const { slug: frontmatterSlug } = await contentEntryType.getEntryInfo({
268
+ fileUrl,
269
+ contents: await fs.promises.readFile(fileUrl, "utf-8")
270
+ });
271
+ return parseEntrySlug({ generatedSlug, frontmatterSlug, id, collection });
272
+ }
273
+ function getExtGlob(exts) {
274
+ return exts.length === 1 ? (
275
+ // Wrapping {...} breaks when there is only one extension
276
+ exts[0]
277
+ ) : `{${exts.join(",")}}`;
278
+ }
241
279
  export {
242
280
  NoCollectionError,
243
281
  collectionConfigParser,
244
282
  contentConfigParser,
245
283
  contentObservable,
284
+ getContentEntryConfigByExtMap,
246
285
  getContentEntryExts,
247
286
  getContentPaths,
248
287
  getDotAstroTypeReference,
@@ -250,8 +289,11 @@ export {
250
289
  getEntryInfo,
251
290
  getEntrySlug,
252
291
  getEntryType,
292
+ getExtGlob,
253
293
  globalContentConfigObserver,
294
+ hasUnderscoreBelowContentDirectoryPath,
254
295
  loadContentConfig,
255
296
  msg,
297
+ parseEntrySlug,
256
298
  parseFrontmatter
257
299
  };
@@ -11,7 +11,6 @@ import {
11
11
  SCRIPTS_PLACEHOLDER,
12
12
  STYLES_PLACEHOLDER
13
13
  } from "./consts.js";
14
- import { getContentEntryExts } from "./utils.js";
15
14
  function isPropagatedAsset(viteId) {
16
15
  const flags = new URLSearchParams(viteId.split("?")[1]);
17
16
  return flags.has(PROPAGATED_ASSET_FLAG);
@@ -21,7 +20,6 @@ function astroContentAssetPropagationPlugin({
21
20
  settings
22
21
  }) {
23
22
  let devModuleLoader;
24
- const contentEntryExts = getContentEntryExts(settings);
25
23
  return {
26
24
  name: "astro:content-asset-propagation",
27
25
  configureServer(server) {
@@ -6,14 +6,15 @@ import { AstroError } from "../core/errors/errors.js";
6
6
  import { escapeViteEnvReferences, getFileInfo } from "../vite-plugin-utils/index.js";
7
7
  import { CONTENT_FLAG } from "./consts.js";
8
8
  import {
9
+ getContentEntryConfigByExtMap,
9
10
  getContentEntryExts,
10
11
  getContentPaths,
11
12
  getEntryData,
12
13
  getEntryInfo,
13
- getEntrySlug,
14
14
  getEntryType,
15
15
  globalContentConfigObserver,
16
- NoCollectionError
16
+ NoCollectionError,
17
+ parseEntrySlug
17
18
  } from "./utils.js";
18
19
  function isContentFlagImport(viteId) {
19
20
  const flags = new URLSearchParams(viteId.split("?")[1]);
@@ -37,12 +38,7 @@ function astroContentImportPlugin({
37
38
  }) {
38
39
  const contentPaths = getContentPaths(settings.config, fs);
39
40
  const contentEntryExts = getContentEntryExts(settings);
40
- const contentEntryExtToParser = /* @__PURE__ */ new Map();
41
- for (const entryType of settings.contentEntryTypes) {
42
- for (const ext of entryType.extensions) {
43
- contentEntryExtToParser.set(ext, entryType);
44
- }
45
- }
41
+ const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
46
42
  const plugins = [
47
43
  {
48
44
  name: "astro:content-imports",
@@ -131,7 +127,7 @@ function astroContentImportPlugin({
131
127
  const contentConfig = await getContentConfigFromGlobal();
132
128
  const rawContents = await fs.promises.readFile(fileId, "utf-8");
133
129
  const fileExt = extname(fileId);
134
- if (!contentEntryExtToParser.has(fileExt)) {
130
+ if (!contentEntryConfigByExt.has(fileExt)) {
135
131
  throw new AstroError({
136
132
  ...AstroErrorData.UnknownContentCollectionError,
137
133
  message: `No parser found for content entry ${JSON.stringify(
@@ -139,13 +135,13 @@ function astroContentImportPlugin({
139
135
  )}. Did you apply an integration for this file type?`
140
136
  });
141
137
  }
142
- const contentEntryParser = contentEntryExtToParser.get(fileExt);
138
+ const contentEntryConfig = contentEntryConfigByExt.get(fileExt);
143
139
  const {
144
140
  rawData,
145
141
  body,
146
- slug: unvalidatedSlug,
142
+ slug: frontmatterSlug,
147
143
  data: unvalidatedData
148
- } = await contentEntryParser.getEntryInfo({
144
+ } = await contentEntryConfig.getEntryInfo({
149
145
  fileUrl: pathToFileURL(fileId),
150
146
  contents: rawContents
151
147
  });
@@ -157,7 +153,12 @@ function astroContentImportPlugin({
157
153
  throw entryInfoResult;
158
154
  const { id, slug: generatedSlug, collection } = entryInfoResult;
159
155
  const _internal = { filePath: fileId, rawData };
160
- const slug = getEntrySlug({ id, collection, slug: generatedSlug, unvalidatedSlug });
156
+ const slug = parseEntrySlug({
157
+ id,
158
+ collection,
159
+ generatedSlug,
160
+ frontmatterSlug
161
+ });
161
162
  const collectionConfig = contentConfig == null ? void 0 : contentConfig.collections[collection];
162
163
  let data = collectionConfig ? await getEntryData(
163
164
  { id, collection, slug, _internal, unvalidatedData },
@@ -1,7 +1,21 @@
1
+ /// <reference types="node" />
2
+ import fsMod from 'node:fs';
1
3
  import type { Plugin } from 'vite';
2
4
  import type { AstroSettings } from '../@types/astro.js';
5
+ import { getContentEntryConfigByExtMap, type ContentPaths } from './utils.js';
3
6
  interface AstroContentVirtualModPluginParams {
4
7
  settings: AstroSettings;
5
8
  }
6
9
  export declare function astroContentVirtualModPlugin({ settings, }: AstroContentVirtualModPluginParams): Plugin;
10
+ /**
11
+ * Generate a map from a collection + slug to the local file path.
12
+ * This is used internally to resolve entry imports when using `getEntryBySlug()`.
13
+ * @see `src/content/virtual-mod.mjs`
14
+ */
15
+ export declare function getStringifiedLookupMap({ contentPaths, contentEntryConfigByExt, root, fs, }: {
16
+ contentEntryConfigByExt: ReturnType<typeof getContentEntryConfigByExtMap>;
17
+ contentPaths: Pick<ContentPaths, 'contentDir' | 'cacheDir'>;
18
+ root: URL;
19
+ fs: typeof fsMod;
20
+ }): Promise<string>;
7
21
  export {};
@@ -1,25 +1,26 @@
1
+ import glob from "fast-glob";
1
2
  import fsMod from "node:fs";
2
- import * as path from "node:path";
3
- import { normalizePath } from "vite";
4
- import { appendForwardSlash, prependForwardSlash } from "../core/path.js";
3
+ import { extname } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { rootRelativePath } from "../core/util.js";
5
6
  import { VIRTUAL_MODULE_ID } from "./consts.js";
6
- import { getContentEntryExts, getContentPaths } from "./utils.js";
7
+ import {
8
+ getContentEntryConfigByExtMap,
9
+ getContentPaths,
10
+ getEntryInfo,
11
+ getEntrySlug,
12
+ getExtGlob,
13
+ hasUnderscoreBelowContentDirectoryPath,
14
+ NoCollectionError
15
+ } from "./utils.js";
7
16
  function astroContentVirtualModPlugin({
8
17
  settings
9
18
  }) {
10
19
  const contentPaths = getContentPaths(settings.config);
11
- const relContentDir = normalizePath(
12
- appendForwardSlash(
13
- prependForwardSlash(
14
- path.relative(settings.config.root.pathname, contentPaths.contentDir.pathname)
15
- )
16
- )
17
- );
18
- const contentEntryExts = getContentEntryExts(settings);
19
- const extGlob = contentEntryExts.length === 1 ? (
20
- // Wrapping {...} breaks when there is only one extension
21
- contentEntryExts[0]
22
- ) : `{${contentEntryExts.join(",")}}`;
20
+ const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);
21
+ const contentEntryConfigByExt = getContentEntryConfigByExtMap(settings);
22
+ const contentEntryExts = [...contentEntryConfigByExt.keys()];
23
+ const extGlob = getExtGlob(contentEntryExts);
23
24
  const entryGlob = `${relContentDir}**/*${extGlob}`;
24
25
  const virtualModContents = fsMod.readFileSync(contentPaths.virtualModTemplate, "utf-8").replace("@@CONTENT_DIR@@", relContentDir).replace("@@ENTRY_GLOB_PATH@@", entryGlob).replace("@@RENDER_ENTRY_GLOB_PATH@@", entryGlob);
25
26
  const astroContentVirtualModuleId = "\0" + VIRTUAL_MODULE_ID;
@@ -31,15 +32,71 @@ function astroContentVirtualModPlugin({
31
32
  return astroContentVirtualModuleId;
32
33
  }
33
34
  },
34
- load(id) {
35
+ async load(id) {
36
+ const stringifiedLookupMap = await getStringifiedLookupMap({
37
+ fs: fsMod,
38
+ contentPaths,
39
+ contentEntryConfigByExt,
40
+ root: settings.config.root
41
+ });
35
42
  if (id === astroContentVirtualModuleId) {
36
43
  return {
37
- code: virtualModContents
44
+ code: virtualModContents.replace(
45
+ "/* @@LOOKUP_MAP_ASSIGNMENT@@ */",
46
+ `lookupMap = ${stringifiedLookupMap};`
47
+ )
38
48
  };
39
49
  }
40
50
  }
41
51
  };
42
52
  }
53
+ async function getStringifiedLookupMap({
54
+ contentPaths,
55
+ contentEntryConfigByExt,
56
+ root,
57
+ fs
58
+ }) {
59
+ const { contentDir } = contentPaths;
60
+ const globOpts = {
61
+ absolute: true,
62
+ cwd: fileURLToPath(root),
63
+ fs: {
64
+ readdir: fs.readdir.bind(fs),
65
+ readdirSync: fs.readdirSync.bind(fs)
66
+ }
67
+ };
68
+ const relContentDir = rootRelativePath(root, contentDir, false);
69
+ const contentGlob = await glob(
70
+ `${relContentDir}**/*${getExtGlob([...contentEntryConfigByExt.keys()])}`,
71
+ globOpts
72
+ );
73
+ let filePathByLookupId = {};
74
+ await Promise.all(
75
+ contentGlob.filter((e) => !hasUnderscoreBelowContentDirectoryPath(pathToFileURL(e), contentDir)).map(async (filePath) => {
76
+ const info = getEntryInfo({ contentDir, entry: filePath });
77
+ if (info instanceof NoCollectionError)
78
+ return;
79
+ const contentEntryType = contentEntryConfigByExt.get(extname(filePath));
80
+ if (!contentEntryType)
81
+ return;
82
+ const { id, collection, slug: generatedSlug } = info;
83
+ const slug = await getEntrySlug({
84
+ id,
85
+ collection,
86
+ generatedSlug,
87
+ fs,
88
+ fileUrl: pathToFileURL(filePath),
89
+ contentEntryType
90
+ });
91
+ filePathByLookupId[collection] = {
92
+ ...filePathByLookupId[collection],
93
+ [slug]: rootRelativePath(root, filePath)
94
+ };
95
+ })
96
+ );
97
+ return JSON.stringify(filePathByLookupId);
98
+ }
43
99
  export {
44
- astroContentVirtualModPlugin
100
+ astroContentVirtualModPlugin,
101
+ getStringifiedLookupMap
45
102
  };
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "2.4.3";
1
+ const ASTRO_VERSION = "2.4.5";
2
2
  const SUPPORTED_MARKDOWN_FILE_EXTENSIONS = [
3
3
  ".markdown",
4
4
  ".mdown",
@@ -53,7 +53,7 @@ async function dev(settings, options) {
53
53
  isRestart: options.isRestart
54
54
  })
55
55
  );
56
- const currentVersion = "2.4.3";
56
+ const currentVersion = "2.4.5";
57
57
  if (currentVersion.includes("-")) {
58
58
  warn(options.logging, null, msg.prerelease({ currentVersion }));
59
59
  }
@@ -47,7 +47,7 @@ function serverStart({
47
47
  base,
48
48
  isRestart = false
49
49
  }) {
50
- const version = "2.4.3";
50
+ const version = "2.4.5";
51
51
  const localPrefix = `${dim("\u2503")} Local `;
52
52
  const networkPrefix = `${dim("\u2503")} Network `;
53
53
  const emptyPrefix = " ".repeat(11);
@@ -233,7 +233,7 @@ function printHelp({
233
233
  message.push(
234
234
  linebreak(),
235
235
  ` ${bgGreen(black(` ${commandName} `))} ${green(
236
- `v${"2.4.3"}`
236
+ `v${"2.4.5"}`
237
237
  )} ${headline}`
238
238
  );
239
239
  }
@@ -37,7 +37,7 @@ export declare function isPage(file: URL, settings: AstroSettings): boolean;
37
37
  export declare function isEndpoint(file: URL, settings: AstroSettings): boolean;
38
38
  export declare function isModeServerWithNoAdapter(settings: AstroSettings): boolean;
39
39
  export declare function relativeToSrcDir(config: AstroConfig, idOrUrl: URL | string): string;
40
- export declare function rootRelativePath(root: URL, idOrUrl: URL | string): string;
40
+ export declare function rootRelativePath(root: URL, idOrUrl: URL | string, shouldPrependForwardSlash?: boolean): string;
41
41
  export declare function emoji(char: string, fallback: string): string;
42
42
  /**
43
43
  * Simulate Vite's resolve and import analysis so we can import the id as an URL
package/dist/core/util.js CHANGED
@@ -114,7 +114,7 @@ function relativeToSrcDir(config, idOrUrl) {
114
114
  }
115
115
  return id.slice(slash(fileURLToPath(config.srcDir)).length);
116
116
  }
117
- function rootRelativePath(root, idOrUrl) {
117
+ function rootRelativePath(root, idOrUrl, shouldPrependForwardSlash = true) {
118
118
  let id;
119
119
  if (typeof idOrUrl !== "string") {
120
120
  id = unwrapId(viteID(idOrUrl));
@@ -125,7 +125,7 @@ function rootRelativePath(root, idOrUrl) {
125
125
  if (id.startsWith(normalizedRoot)) {
126
126
  id = id.slice(normalizedRoot.length);
127
127
  }
128
- return prependForwardSlash(id);
128
+ return shouldPrependForwardSlash ? prependForwardSlash(id) : id;
129
129
  }
130
130
  function emoji(char, fallback) {
131
131
  return process.platform !== "win32" ? char : fallback;
@@ -5,20 +5,22 @@ const getConfigAlias = (settings) => {
5
5
  if (!tsConfig || !tsConfigPath || !tsConfig.compilerOptions)
6
6
  return null;
7
7
  const { baseUrl, paths } = tsConfig.compilerOptions;
8
- if (!baseUrl || !paths)
8
+ if (!baseUrl)
9
9
  return null;
10
10
  const resolvedBaseUrl = path.resolve(path.dirname(tsConfigPath), baseUrl);
11
11
  const aliases = [];
12
- for (const [alias, values] of Object.entries(paths)) {
13
- const find = new RegExp(
14
- `^${[...alias].map(
15
- (segment) => segment === "*" ? "(.+)" : segment.replace(/[\\^$*+?.()|[\]{}]/, "\\$&")
16
- ).join("")}$`
17
- );
18
- let matchId = 0;
19
- for (const value of values) {
20
- const replacement = [...normalizePath(path.resolve(resolvedBaseUrl, value))].map((segment) => segment === "*" ? `$${++matchId}` : segment === "$" ? "$$" : segment).join("");
21
- aliases.push({ find, replacement });
12
+ if (paths) {
13
+ for (const [alias, values] of Object.entries(paths)) {
14
+ const find = new RegExp(
15
+ `^${[...alias].map(
16
+ (segment) => segment === "*" ? "(.+)" : segment.replace(/[\\^$*+?.()|[\]{}]/, "\\$&")
17
+ ).join("")}$`
18
+ );
19
+ let matchId = 0;
20
+ for (const value of values) {
21
+ const replacement = [...normalizePath(path.resolve(resolvedBaseUrl, value))].map((segment) => segment === "*" ? `$${++matchId}` : segment === "$" ? "$$" : segment).join("");
22
+ aliases.push({ find, replacement });
23
+ }
22
24
  }
23
25
  }
24
26
  aliases.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
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",
@@ -28,6 +28,18 @@ const collectionToEntryMap = createCollectionToGlobResultMap({
28
28
  contentDir,
29
29
  });
30
30
 
31
+ let lookupMap = {};
32
+ /* @@LOOKUP_MAP_ASSIGNMENT@@ */
33
+
34
+ function createGlobLookup(glob) {
35
+ return async (collection, lookupId) => {
36
+ const filePath = lookupMap[collection]?.[lookupId];
37
+
38
+ if (!filePath) return undefined;
39
+ return glob[collection][filePath];
40
+ };
41
+ }
42
+
31
43
  const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
32
44
  query: { astroPropagatedAssets: true },
33
45
  });
@@ -38,10 +50,10 @@ const collectionToRenderEntryMap = createCollectionToGlobResultMap({
38
50
 
39
51
  export const getCollection = createGetCollection({
40
52
  collectionToEntryMap,
41
- collectionToRenderEntryMap,
53
+ getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
42
54
  });
43
55
 
44
56
  export const getEntryBySlug = createGetEntryBySlug({
45
- getCollection,
46
- collectionToRenderEntryMap,
57
+ getEntryImport: createGlobLookup(collectionToEntryMap),
58
+ getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
47
59
  });