astro 7.2.1 → 7.2.2

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.
@@ -15,3 +15,11 @@ export declare const FONT_FORMATS: Array<{
15
15
  }>;
16
16
  export declare const GENERIC_FALLBACK_NAMES: readonly ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong"];
17
17
  export declare const FONTS_TYPES_FILE = "fonts.d.ts";
18
+ /**
19
+ * Variable name used in the font-file-url-resolver virtual module to hold
20
+ * the ephemeral font HTTP server address. The incremental build plugin
21
+ * strips the variable declaration (which contains an OS-assigned port that
22
+ * changes every build) from the module source before hashing so that the
23
+ * dependency hash is deterministic across builds.
24
+ */
25
+ export declare const FONTS_SERVER_ADDRESS_PLACEHOLDER = "__ASTRO_FONTS_SERVER_ADDRESS__";
@@ -39,10 +39,12 @@ const GENERIC_FALLBACK_NAMES = [
39
39
  "fangsong"
40
40
  ];
41
41
  const FONTS_TYPES_FILE = "fonts.d.ts";
42
+ const FONTS_SERVER_ADDRESS_PLACEHOLDER = "__ASTRO_FONTS_SERVER_ADDRESS__";
42
43
  export {
43
44
  ASSETS_DIR,
44
45
  CACHE_DIR,
45
46
  DEFAULTS,
47
+ FONTS_SERVER_ADDRESS_PLACEHOLDER,
46
48
  FONTS_TYPES_FILE,
47
49
  FONT_FORMATS,
48
50
  FONT_TYPES,
@@ -13,6 +13,7 @@ import {
13
13
  ASSETS_DIR,
14
14
  CACHE_DIR,
15
15
  DEFAULTS,
16
+ FONTS_SERVER_ADDRESS_PLACEHOLDER,
16
17
  RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID,
17
18
  RESOLVED_RUNTIME_VIRTUAL_MODULE_ID,
18
19
  RESOLVED_VIRTUAL_MODULE_ID,
@@ -285,9 +286,10 @@ function fontsPlugin({ settings, sync, logger }) {
285
286
  return {
286
287
  code: `
287
288
  import { RemoteRuntimeFontFileUrlResolver } from ${JSON.stringify(new URL("./infra/remote-runtime-font-file-url-resolver.js", import.meta.url))};
289
+ const ${FONTS_SERVER_ADDRESS_PLACEHOLDER} = ${JSON.stringify(serverAddress)};
288
290
  export const runtimeFontFileUrlResolver = new RemoteRuntimeFontFileUrlResolver({
289
291
  urls: new Set(${JSON.stringify(urls)}),
290
- address: ${JSON.stringify(serverAddress)},
292
+ address: ${FONTS_SERVER_ADDRESS_PLACEHOLDER},
291
293
  });
292
294
  `
293
295
  };
@@ -36,6 +36,35 @@ async function handleSvgDeduplication(fileData, filename, fileEmitter) {
36
36
  return handle;
37
37
  }
38
38
  }
39
+ const TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set(["EMFILE", "ENFILE", "EAGAIN", "EBUSY"]);
40
+ const MAX_CONCURRENT_READS = 200;
41
+ let activeReads = 0;
42
+ const readQueue = [];
43
+ async function readFileWithRetry(url, maxRetries = 5) {
44
+ if (activeReads >= MAX_CONCURRENT_READS) {
45
+ await new Promise((resolve) => readQueue.push(resolve));
46
+ }
47
+ activeReads++;
48
+ try {
49
+ for (let attempt = 0; ; attempt++) {
50
+ try {
51
+ return await fs.readFile(url);
52
+ } catch (err) {
53
+ const code = err instanceof Error && "code" in err ? err.code : void 0;
54
+ if (code && TRANSIENT_ERROR_CODES.has(code) && attempt < maxRetries) {
55
+ await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
56
+ continue;
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+ } finally {
62
+ activeReads--;
63
+ if (readQueue.length > 0) {
64
+ readQueue.shift()();
65
+ }
66
+ }
67
+ }
39
68
  async function emitImageMetadata(id, fileEmitter) {
40
69
  if (!id) {
41
70
  return void 0;
@@ -43,9 +72,12 @@ async function emitImageMetadata(id, fileEmitter) {
43
72
  const url = pathToFileURL(id);
44
73
  let fileData;
45
74
  try {
46
- fileData = await fs.readFile(url);
47
- } catch {
48
- return void 0;
75
+ fileData = await readFileWithRetry(url);
76
+ } catch (err) {
77
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
78
+ return void 0;
79
+ }
80
+ throw err;
49
81
  }
50
82
  const fileMetadata = await imageMetadata(fileData, id);
51
83
  const emittedImage = {
@@ -128,7 +128,7 @@ Run \`astro dev --help\` to see available commands.`
128
128
  }
129
129
  const root = pathToFileURL(resolveRoot(flags.root) + "/");
130
130
  if (ignoreLock) {
131
- const existingServer2 = checkExistingServer(root);
131
+ const existingServer2 = await checkExistingServer(root);
132
132
  if (existingServer2) {
133
133
  logger.info(
134
134
  "SKIP_FORMAT",
@@ -141,7 +141,7 @@ Run \`astro dev --help\` to see available commands.`
141
141
  const inlineConfig2 = flagsToAstroInlineConfig(flags);
142
142
  return await devServer(inlineConfig2);
143
143
  }
144
- const existingServer = checkExistingServer(root);
144
+ const existingServer = await checkExistingServer(root);
145
145
  if (existingServer) {
146
146
  if (flags.force) {
147
147
  await killDevServer(root, existingServer);
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.2.1";
3
+ version = "7.2.2";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -69,7 +69,7 @@ Run \`astro preview --help\` to see available commands.`
69
69
  process.exit(1);
70
70
  }
71
71
  const root = pathToFileURL(resolveRoot(flags.root) + "/");
72
- const existingServer = checkExistingServer(root, "preview");
72
+ const existingServer = await checkExistingServer(root, "preview");
73
73
  if (existingServer) {
74
74
  const message = [
75
75
  "Another astro preview server is already running.",
@@ -101,7 +101,7 @@ async function background({
101
101
  config
102
102
  }) {
103
103
  const root = getRootURL(flags);
104
- const existing = checkExistingServer(root, config.command);
104
+ const existing = await checkExistingServer(root, config.command);
105
105
  if (existing && !flags.force) {
106
106
  logger.info("SKIP_FORMAT", formatServerRunningMessage(existing, config, { existing: true }));
107
107
  return;
@@ -160,7 +160,7 @@ async function stop({
160
160
  config
161
161
  }) {
162
162
  const root = getRootURL(flags);
163
- const existing = checkExistingServer(root, config.command);
163
+ const existing = await checkExistingServer(root, config.command);
164
164
  if (!existing) {
165
165
  logger.info("SKIP_FORMAT", `No ${config.command} server is running.`);
166
166
  return;
@@ -174,7 +174,7 @@ async function status({
174
174
  config
175
175
  }) {
176
176
  const root = getRootURL(flags);
177
- const existing = checkExistingServer(root, config.command);
177
+ const existing = await checkExistingServer(root, config.command);
178
178
  if (!existing) {
179
179
  logger.info("SKIP_FORMAT", `No ${config.command} server is running.`);
180
180
  return;
@@ -198,7 +198,7 @@ async function logs({
198
198
  config
199
199
  }) {
200
200
  const root = getRootURL(flags);
201
- const existing = checkExistingServer(root, config.command);
201
+ const existing = await checkExistingServer(root, config.command);
202
202
  if (!existing) {
203
203
  logger.error("SKIP_FORMAT", `No ${config.command} server is running.`);
204
204
  process.exit(1);
@@ -196,7 +196,7 @@ ${contentConfig.error.message}`
196
196
  logger.info("Content config changed");
197
197
  shouldClear = true;
198
198
  }
199
- if (previousAstroVersion && previousAstroVersion !== "7.2.1") {
199
+ if (previousAstroVersion && previousAstroVersion !== "7.2.2") {
200
200
  logger.info("Astro version changed");
201
201
  shouldClear = true;
202
202
  }
@@ -204,8 +204,8 @@ ${contentConfig.error.message}`
204
204
  logger.info("Clearing content store");
205
205
  this.#store.clearAll();
206
206
  }
207
- if ("7.2.1") {
208
- this.#store.metaStore().set("astro-version", "7.2.1");
207
+ if ("7.2.2") {
208
+ this.#store.metaStore().set("astro-version", "7.2.2");
209
209
  }
210
210
  if (currentConfigDigest) {
211
211
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -2,7 +2,11 @@ import { existsSync, promises as fs } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import * as yaml from "js-yaml";
4
4
  import * as toml from "smol-toml";
5
- import { FileGlobNotSupported, FileParserNotFound } from "../../core/errors/errors-data.js";
5
+ import {
6
+ DuplicateContentEntrySlugError,
7
+ FileGlobNotSupported,
8
+ FileParserNotFound
9
+ } from "../../core/errors/errors-data.js";
6
10
  import { AstroError } from "../../core/errors/index.js";
7
11
  import { posixRelative } from "../utils.js";
8
12
  function file(fileName, options) {
@@ -27,7 +31,7 @@ function file(fileName, options) {
27
31
  message: FileParserNotFound.message(fileName)
28
32
  });
29
33
  }
30
- async function syncData(filePath, { logger, parseData, store, config }) {
34
+ async function syncData(filePath, { logger, parseData, store, config, collection }) {
31
35
  let data;
32
36
  try {
33
37
  const contents = await fs.readFile(filePath, "utf-8");
@@ -52,9 +56,20 @@ function file(fileName, options) {
52
56
  continue;
53
57
  }
54
58
  if (idList.has(id)) {
55
- logger.warn(
56
- `Duplicate id "${id}" found in ${fileName}. Later items with the same id will overwrite earlier ones.`
59
+ const message = DuplicateContentEntrySlugError.message(
60
+ collection,
61
+ id,
62
+ fileName,
63
+ fileName
57
64
  );
65
+ if (config.prerenderConflictBehavior === "error") {
66
+ throw new AstroError({
67
+ ...DuplicateContentEntrySlugError,
68
+ message
69
+ });
70
+ } else if (config.prerenderConflictBehavior !== "ignore") {
71
+ logger.warn(message);
72
+ }
58
73
  }
59
74
  idList.add(id);
60
75
  const parsedData = await parseData({ id, data: rawItem, filePath });
@@ -5,6 +5,8 @@ import pLimit from "p-limit";
5
5
  import colors from "piccolore";
6
6
  import picomatch from "picomatch";
7
7
  import { glob as tinyglobby } from "tinyglobby";
8
+ import * as AstroErrorData from "../../core/errors/errors-data.js";
9
+ import { AstroError } from "../../core/errors/index.js";
8
10
  import { getContentEntryIdAndSlug, posixRelative } from "../utils.js";
9
11
  function generateIdDefault({ entry, base, data }, isLegacy) {
10
12
  if (data.slug) {
@@ -107,9 +109,20 @@ function glob(globOptions) {
107
109
  if (existingEntry && existingEntry.filePath && existingEntry.filePath !== relativePath2) {
108
110
  const oldFilePath = new URL(existingEntry.filePath, config.root);
109
111
  if (existsSync(oldFilePath)) {
110
- logger.warn(
111
- `Duplicate id "${id}" found in ${filePath2}. Later items with the same id will overwrite earlier ones.`
112
+ const message = AstroErrorData.DuplicateContentEntrySlugError.message(
113
+ collection,
114
+ id,
115
+ existingEntry.filePath,
116
+ relativePath2
112
117
  );
118
+ if (config.prerenderConflictBehavior === "error") {
119
+ throw new AstroError({
120
+ ...AstroErrorData.DuplicateContentEntrySlugError,
121
+ message
122
+ });
123
+ } else if (config.prerenderConflictBehavior !== "ignore") {
124
+ logger.warn(message);
125
+ }
113
126
  }
114
127
  }
115
128
  if (entryType.getRenderFunction && !globOptions.deferRender) {
@@ -222,7 +222,16 @@ ${colors.bgGreen(colors.black(` ${verb} static routes `))}`);
222
222
  if (prerenderer.collectStaticImages) {
223
223
  const adapterImages = await prerenderer.collectStaticImages();
224
224
  for (const [path, entry] of adapterImages) {
225
- staticImageList.set(path, entry);
225
+ const existing = staticImageList.get(path);
226
+ if (existing) {
227
+ for (const [hash, transform] of entry.transforms) {
228
+ if (!existing.transforms.has(hash)) {
229
+ existing.transforms.set(hash, transform);
230
+ }
231
+ }
232
+ } else {
233
+ staticImageList.set(path, entry);
234
+ }
226
235
  }
227
236
  }
228
237
  } finally {
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { FONTS_SERVER_ADDRESS_PLACEHOLDER } from "../../../assets/fonts/constants.js";
2
3
  import { PROPAGATED_ASSET_FLAG } from "../../../content/consts.js";
3
4
  import { hasContentFlag } from "../../../content/utils.js";
4
5
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../../constants.js";
@@ -7,29 +8,13 @@ import { rootRelativePath } from "../../viteUtils.js";
7
8
  import { moduleIsTopLevelPage } from "../graph.js";
8
9
  import { isContentDataIncrementalModule } from "../incremental-metadata.js";
9
10
  import { getPageDataByViteID } from "../internal.js";
10
- function collectTransitiveDeps(graph, rootId) {
11
- const deps = /* @__PURE__ */ new Set();
12
- const queue = [rootId];
13
- while (queue.length > 0) {
14
- const current = queue.pop();
15
- if (deps.has(current)) continue;
16
- const modInfo = graph.getModuleInfo(current);
17
- if (isContentDataIncrementalModule(modInfo)) continue;
18
- deps.add(current);
19
- if (!modInfo) continue;
20
- for (const dep of modInfo.importedIds) {
21
- if (!deps.has(dep)) queue.push(dep);
22
- }
23
- for (const dep of modInfo.dynamicallyImportedIds) {
24
- if (!deps.has(dep)) queue.push(dep);
25
- }
26
- }
27
- return [...deps].sort();
28
- }
29
11
  const ASSET_PLACEHOLDERS = [
30
12
  { token: "__ASTRO_ASSET_IMAGE__", pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g },
31
13
  { token: "__VITE_ASSET__", pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }
32
14
  ];
15
+ const FONTS_ADDRESS_DECLARATION = new RegExp(
16
+ `(?:const|let|var)\\s+${FONTS_SERVER_ADDRESS_PLACEHOLDER}\\s*=[^;]+;`
17
+ );
33
18
  function resolveAssetPlaceholders(graph, code) {
34
19
  let resolved = code;
35
20
  for (const { token, pattern } of ASSET_PLACEHOLDERS) {
@@ -42,6 +27,9 @@ function resolveAssetPlaceholders(graph, code) {
42
27
  }
43
28
  });
44
29
  }
30
+ if (resolved.includes(FONTS_SERVER_ADDRESS_PLACEHOLDER)) {
31
+ resolved = resolved.replace(FONTS_ADDRESS_DECLARATION, "");
32
+ }
45
33
  return resolved;
46
34
  }
47
35
  function hashModules(graph, sortedIds) {
@@ -57,11 +45,131 @@ function hashModules(graph, sortedIds) {
57
45
  }
58
46
  return hasher.digest("hex");
59
47
  }
60
- function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint, hashesByComponent) {
48
+ function createTransitiveGraphCache(graph) {
49
+ const modules = /* @__PURE__ */ new Map();
50
+ const dependencies = /* @__PURE__ */ new Map();
51
+ const excludedModules = /* @__PURE__ */ new Set();
52
+ const pending = [...graph.getModuleIds()];
53
+ for (const id of pending) {
54
+ if (modules.has(id)) continue;
55
+ const info = graph.getModuleInfo(id);
56
+ modules.set(id, info);
57
+ if (isContentDataIncrementalModule(info)) {
58
+ excludedModules.add(id);
59
+ continue;
60
+ }
61
+ const importedIds = [...info?.importedIds ?? [], ...info?.dynamicallyImportedIds ?? []];
62
+ dependencies.set(id, importedIds);
63
+ pending.push(...importedIds);
64
+ }
65
+ for (const id of excludedModules) modules.delete(id);
66
+ for (const [id, importedIds] of dependencies) {
67
+ dependencies.set(
68
+ id,
69
+ importedIds.filter((importedId) => !excludedModules.has(importedId))
70
+ );
71
+ }
72
+ const reverseDependencies = /* @__PURE__ */ new Map();
73
+ for (const id of modules.keys()) reverseDependencies.set(id, []);
74
+ for (const [id, importedIds] of dependencies) {
75
+ for (const importedId of importedIds) reverseDependencies.get(importedId)?.push(id);
76
+ }
77
+ const visited = /* @__PURE__ */ new Set();
78
+ const finishOrder = [];
79
+ for (const rootId of modules.keys()) {
80
+ if (visited.has(rootId)) continue;
81
+ visited.add(rootId);
82
+ const stack = [[rootId, 0]];
83
+ while (stack.length > 0) {
84
+ const frame = stack[stack.length - 1];
85
+ const importedIds = dependencies.get(frame[0]) ?? [];
86
+ if (frame[1] < importedIds.length) {
87
+ const importedId = importedIds[frame[1]++];
88
+ if (!visited.has(importedId)) {
89
+ visited.add(importedId);
90
+ stack.push([importedId, 0]);
91
+ }
92
+ } else {
93
+ finishOrder.push(frame[0]);
94
+ stack.pop();
95
+ }
96
+ }
97
+ }
98
+ const componentByModule = /* @__PURE__ */ new Map();
99
+ const components = [];
100
+ for (const rootId of finishOrder.toReversed()) {
101
+ if (componentByModule.has(rootId)) continue;
102
+ const componentIndex = components.length;
103
+ const component = [];
104
+ const stack = [rootId];
105
+ componentByModule.set(rootId, componentIndex);
106
+ while (stack.length > 0) {
107
+ const id = stack.pop();
108
+ component.push(id);
109
+ for (const importerId of reverseDependencies.get(id) ?? []) {
110
+ if (!componentByModule.has(importerId)) {
111
+ componentByModule.set(importerId, componentIndex);
112
+ stack.push(importerId);
113
+ }
114
+ }
115
+ }
116
+ components.push(component.sort());
117
+ }
118
+ const componentDependencies = components.map(() => /* @__PURE__ */ new Set());
119
+ const componentImporters = components.map(() => /* @__PURE__ */ new Set());
120
+ for (const [id, importedIds] of dependencies) {
121
+ const componentIndex = componentByModule.get(id);
122
+ for (const importedId of importedIds) {
123
+ const dependencyIndex = componentByModule.get(importedId);
124
+ if (dependencyIndex === componentIndex) continue;
125
+ componentDependencies[componentIndex].add(dependencyIndex);
126
+ componentImporters[dependencyIndex].add(componentIndex);
127
+ }
128
+ }
129
+ const componentHashes = /* @__PURE__ */ new Map();
130
+ const componentHasServerIsland = /* @__PURE__ */ new Map();
131
+ const unresolvedDependencies = componentDependencies.map((items) => items.size);
132
+ const ready = unresolvedDependencies.flatMap((count, index) => count === 0 ? [index] : []);
133
+ for (const componentIndex of ready) {
134
+ const hasher = crypto.createHash("sha256");
135
+ hasher.update(hashModules(graph, components[componentIndex]));
136
+ const dependencyHashes = [...componentDependencies[componentIndex]].map((dependencyIndex) => componentHashes.get(dependencyIndex)).sort();
137
+ for (const dependencyHash of dependencyHashes) {
138
+ hasher.update("\n");
139
+ hasher.update(dependencyHash);
140
+ }
141
+ componentHashes.set(componentIndex, hasher.digest("hex"));
142
+ componentHasServerIsland.set(
143
+ componentIndex,
144
+ components[componentIndex].some(
145
+ (id) => (modules.get(id)?.meta?.astro?.serverComponents?.length ?? 0) > 0
146
+ ) || [...componentDependencies[componentIndex]].some(
147
+ (dependencyIndex) => componentHasServerIsland.get(dependencyIndex)
148
+ )
149
+ );
150
+ for (const importerIndex of componentImporters[componentIndex]) {
151
+ unresolvedDependencies[importerIndex]--;
152
+ if (unresolvedDependencies[importerIndex] === 0) ready.push(importerIndex);
153
+ }
154
+ }
155
+ return {
156
+ hashes: new Map(
157
+ [...componentByModule].map(([id, componentIndex]) => [
158
+ id,
159
+ componentHashes.get(componentIndex)
160
+ ])
161
+ ),
162
+ serverIslandModules: new Set(
163
+ [...componentByModule].filter(([, componentIndex]) => componentHasServerIsland.get(componentIndex)).map(([id]) => id)
164
+ )
165
+ };
166
+ }
167
+ function collectClientEntrypointHashes(transitiveHashes, entrypointIds, pagesByEntrypoint, hashesByComponent) {
61
168
  for (const entrypointId of entrypointIds) {
62
169
  const pages = pagesByEntrypoint.get(entrypointId);
63
170
  if (!pages?.size) continue;
64
- const hash = hashModules(graph, collectTransitiveDeps(graph, entrypointId));
171
+ const hash = transitiveHashes.get(entrypointId);
172
+ if (!hash) continue;
65
173
  for (const pageData of pages) {
66
174
  let list = hashesByComponent.get(pageData.component);
67
175
  if (!list) {
@@ -75,15 +183,16 @@ function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint,
75
183
  function foldClientDependencies(graph, internals) {
76
184
  const baseHashes = internals.pageDependencyHashes;
77
185
  if (!baseHashes) return;
186
+ const { hashes: transitiveHashes } = createTransitiveGraphCache(graph);
78
187
  const hashesByComponent = /* @__PURE__ */ new Map();
79
188
  collectClientEntrypointHashes(
80
- graph,
189
+ transitiveHashes,
81
190
  internals.discoveredClientOnlyComponents.keys(),
82
191
  internals.pagesByClientOnly,
83
192
  hashesByComponent
84
193
  );
85
194
  collectClientEntrypointHashes(
86
- graph,
195
+ transitiveHashes,
87
196
  internals.discoveredScripts,
88
197
  internals.pagesByScriptId,
89
198
  hashesByComponent
@@ -98,24 +207,17 @@ function foldClientDependencies(graph, internals) {
98
207
  baseHashes.set(component, hasher.digest("hex"));
99
208
  }
100
209
  }
101
- function collectContentEntryHashes(graph, root) {
210
+ function collectContentEntryHashes(graph, root, transitiveHashes) {
102
211
  const entryHashes = /* @__PURE__ */ new Map();
103
212
  for (const id of graph.getModuleIds()) {
104
213
  if (!hasContentFlag(id, PROPAGATED_ASSET_FLAG)) continue;
105
214
  const renderModuleId = removeQueryString(id);
106
215
  const key = rootRelativePath(root, renderModuleId, false);
107
- const deps = collectTransitiveDeps(graph, renderModuleId);
108
- entryHashes.set(key, hashModules(graph, deps));
216
+ const hash = transitiveHashes.get(renderModuleId);
217
+ if (hash) entryHashes.set(key, hash);
109
218
  }
110
219
  return entryHashes;
111
220
  }
112
- function pageContainsServerIsland(graph, ids) {
113
- for (const id of ids) {
114
- const serverComponents = graph.getModuleInfo(id)?.meta?.astro?.serverComponents;
115
- if (serverComponents?.length) return true;
116
- }
117
- return false;
118
- }
119
221
  function pluginIncremental(internals, root) {
120
222
  return {
121
223
  name: "@astro/plugin-incremental",
@@ -127,6 +229,7 @@ function pluginIncremental(internals, root) {
127
229
  foldClientDependencies(this, internals);
128
230
  return;
129
231
  }
232
+ const transitiveGraph = createTransitiveGraphCache(this);
130
233
  const hashes = /* @__PURE__ */ new Map();
131
234
  const serverIslandComponents = /* @__PURE__ */ new Set();
132
235
  for (const id of this.getModuleIds()) {
@@ -135,14 +238,19 @@ function pluginIncremental(internals, root) {
135
238
  if (!moduleIsTopLevelPage(info)) continue;
136
239
  const pageData = getPageDataByViteID(internals, info.id);
137
240
  if (!pageData) continue;
138
- const deps = collectTransitiveDeps(this, info.id);
139
- hashes.set(pageData.component, hashModules(this, deps));
140
- if (pageContainsServerIsland(this, deps)) {
241
+ const hash = transitiveGraph.hashes.get(info.id);
242
+ if (!hash) continue;
243
+ hashes.set(pageData.component, hash);
244
+ if (transitiveGraph.serverIslandModules.has(info.id)) {
141
245
  serverIslandComponents.add(pageData.component);
142
246
  }
143
247
  }
144
248
  internals.pageDependencyHashes = hashes;
145
- internals.contentEntryRenderHashes = collectContentEntryHashes(this, root);
249
+ internals.contentEntryRenderHashes = collectContentEntryHashes(
250
+ this,
251
+ root,
252
+ transitiveGraph.hashes
253
+ );
146
254
  internals.serverIslandPageComponents = serverIslandComponents;
147
255
  }
148
256
  };
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "7.2.1";
1
+ const ASTRO_VERSION = "7.2.2";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const ASTRO_ERROR_HEADER = "X-Astro-Error";
4
4
  const DEFAULT_404_COMPONENT = "astro-default-404.astro";
@@ -26,7 +26,7 @@ async function dev(inlineConfig) {
26
26
  await telemetry.record([]);
27
27
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
28
28
  const logger = restart.container.logger;
29
- const currentVersion = "7.2.1";
29
+ const currentVersion = "7.2.2";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -32,6 +32,22 @@ export declare function serializeLockFile(data: LockFileData): string;
32
32
  * Signal 0 does not kill the process — it only checks whether the process exists.
33
33
  */
34
34
  export declare function isProcessAlive(pid: number): boolean;
35
+ /**
36
+ * Check whether a process command points to the Astro CLI.
37
+ */
38
+ export declare function isAstroCommand(command: string): boolean;
39
+ interface ProcessInfo {
40
+ pid: number;
41
+ cmd?: string;
42
+ }
43
+ type ProcessLookup = (by: 'pid', value: number, options: {
44
+ logLevel: 'error';
45
+ }) => Promise<ProcessInfo[]>;
46
+ /**
47
+ * Check whether the live process recorded in a lock file is still Astro.
48
+ * If the command cannot be inspected, keep the existing PID-only behavior.
49
+ */
50
+ export declare function isLockFileProcessAlive(data: LockFileData, find?: ProcessLookup): Promise<boolean>;
35
51
  /**
36
52
  * Read the lock file from disk. Returns null if it doesn't exist or is invalid.
37
53
  */
@@ -58,8 +74,9 @@ export declare function evaluateExistingServer(data: LockFileData | null, alive:
58
74
  */
59
75
  export declare function killDevServer(root: URL, data: LockFileData): Promise<void>;
60
76
  /**
61
- * Check for an existing server by reading the lock file and checking process liveness.
77
+ * Check for an existing server by reading the lock file and checking process identity.
62
78
  * Automatically cleans up stale lock files.
63
79
  * Returns the server info if a live server is found, null otherwise.
64
80
  */
65
- export declare function checkExistingServer(root: URL, command?: ServerCommand): LockFileData | null;
81
+ export declare function checkExistingServer(root: URL, command?: ServerCommand): Promise<LockFileData | null>;
82
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
+ import findProcess from "find-process";
3
4
  const GRACEFUL_SHUTDOWN_TIMEOUT = 5e3;
4
5
  function getLockFileURL(root, command = "dev") {
5
6
  return new URL(`.astro/${command}.json`, root);
@@ -42,6 +43,23 @@ function isProcessAlive(pid) {
42
43
  return false;
43
44
  }
44
45
  }
46
+ const ASTRO_COMMAND_PATTERN = /(?:^|[\\/\s"'])(?:astro[\\/]bin[\\/]astro\.mjs|\.bin[\\/]astro(?:\.cmd)?)(?=$|[\s"'])/i;
47
+ function isAstroCommand(command) {
48
+ return ASTRO_COMMAND_PATTERN.test(command);
49
+ }
50
+ async function isLockFileProcessAlive(data, find = findProcess) {
51
+ if (!isProcessAlive(data.pid)) {
52
+ return false;
53
+ }
54
+ try {
55
+ const processInfo = (await find("pid", data.pid, { logLevel: "error" })).find(
56
+ ({ pid }) => pid === data.pid
57
+ );
58
+ return processInfo?.cmd === void 0 || isAstroCommand(processInfo.cmd);
59
+ } catch {
60
+ return true;
61
+ }
62
+ }
45
63
  function readLockFile(root, command = "dev") {
46
64
  const lockFileURL = getLockFileURL(root, command);
47
65
  try {
@@ -98,9 +116,12 @@ async function killDevServer(root, data) {
98
116
  }
99
117
  removeLockFile(root);
100
118
  }
101
- function checkExistingServer(root, command = "dev") {
119
+ async function checkExistingServer(root, command = "dev") {
102
120
  const data = readLockFile(root, command);
103
- const result = evaluateExistingServer(data, data !== null && isProcessAlive(data.pid));
121
+ const result = evaluateExistingServer(
122
+ data,
123
+ data !== null && await isLockFileProcessAlive(data)
124
+ );
104
125
  if (result === null) {
105
126
  return null;
106
127
  }
@@ -115,6 +136,8 @@ export {
115
136
  checkExistingServer,
116
137
  evaluateExistingServer,
117
138
  getLogFileURL,
139
+ isAstroCommand,
140
+ isLockFileProcessAlive,
118
141
  isProcessAlive,
119
142
  killDevServer,
120
143
  parseLockFile,
@@ -270,7 +270,7 @@ function printHelp({
270
270
  message.push(
271
271
  linebreak(),
272
272
  ` ${bgGreen(black(` ${commandName} `))} ${green(
273
- `v${"7.2.1"}`
273
+ `v${"7.2.2"}`
274
274
  )} ${headline}`
275
275
  );
276
276
  }
package/dist/core/util.js CHANGED
@@ -47,8 +47,8 @@ function resolvePages(config) {
47
47
  return new URL("./pages", config.srcDir);
48
48
  }
49
49
  function isInPagesDir(file, config) {
50
- const pagesDir = resolvePages(config);
51
- return file.toString().startsWith(pagesDir.toString());
50
+ const pagesDir = `${resolvePages(config).toString()}/`;
51
+ return file.toString().startsWith(pagesDir);
52
52
  }
53
53
  function isInjectedRoute(file, settings) {
54
54
  let fileURL = file.toString();
@@ -76,7 +76,7 @@ function astroDevCssPlugin({
76
76
  server = viteServer;
77
77
  },
78
78
  applyToEnvironment(env) {
79
- return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
79
+ return command === "dev" && env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
80
80
  },
81
81
  resolveId: {
82
82
  filter: {
@@ -163,7 +163,7 @@ function astroDevCssPlugin({
163
163
  {
164
164
  name: MODULE_DEV_CSS_ALL,
165
165
  applyToEnvironment(env) {
166
- return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
166
+ return command === "dev" && env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
167
167
  },
168
168
  resolveId: {
169
169
  filter: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "7.2.1",
3
+ "version": "7.2.2",
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",
@@ -112,6 +112,7 @@
112
112
  "dset": "^3.1.4",
113
113
  "es-module-lexer": "^2.0.0",
114
114
  "esbuild": "^0.28.0",
115
+ "find-process": "^2.1.1",
115
116
  "flattie": "^1.1.1",
116
117
  "fontace": "~0.4.1",
117
118
  "get-tsconfig": "5.0.0-beta.4",
@@ -145,9 +146,9 @@
145
146
  "xxhash-wasm": "^1.1.0",
146
147
  "yargs-parser": "^22.0.0",
147
148
  "zod": "^4.3.6",
148
- "@astrojs/internal-helpers": "0.10.2",
149
+ "@astrojs/telemetry": "3.3.3",
149
150
  "@astrojs/markdown-satteri": "0.3.5",
150
- "@astrojs/telemetry": "3.3.3"
151
+ "@astrojs/internal-helpers": "0.10.2"
151
152
  },
152
153
  "optionalDependencies": {
153
154
  "sharp": "^0.34.0 || ^0.35.0"
@@ -185,8 +186,8 @@
185
186
  "typescript": "^6.0.3",
186
187
  "undici": "^7.22.0",
187
188
  "vitest": "^4.1.0",
188
- "@astrojs/check": "0.9.10",
189
189
  "@astrojs/markdown-remark": "7.2.2",
190
+ "@astrojs/check": "0.9.10",
190
191
  "astro-scripts": "0.0.14"
191
192
  },
192
193
  "engines": {