astro 7.1.6 → 7.2.1

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 (122) hide show
  1. package/dist/assets/build/generate.d.ts +8 -1
  2. package/dist/assets/build/generate.js +21 -1
  3. package/dist/assets/fonts/providers/index.js +12 -2
  4. package/dist/assets/types.d.ts +13 -0
  5. package/dist/assets/utils/resolveImports.d.ts +0 -1
  6. package/dist/assets/utils/resolveImports.js +1 -4
  7. package/dist/assets/vite-plugin-assets.js +8 -0
  8. package/dist/cli/add/index.js +1 -1
  9. package/dist/cli/agent.d.ts +1 -0
  10. package/dist/cli/agent.js +11 -0
  11. package/dist/cli/dev/background.d.ts +3 -16
  12. package/dist/cli/dev/background.js +7 -93
  13. package/dist/cli/dev/index.d.ts +12 -0
  14. package/dist/cli/dev/index.js +30 -23
  15. package/dist/cli/dev/logs.js +2 -64
  16. package/dist/cli/dev/status.d.ts +2 -9
  17. package/dist/cli/dev/status.js +6 -24
  18. package/dist/cli/dev/stop.d.ts +2 -6
  19. package/dist/cli/dev/stop.js +6 -14
  20. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  21. package/dist/cli/install-package.js +1 -1
  22. package/dist/cli/preview/index.js +77 -3
  23. package/dist/cli/server.d.ts +60 -0
  24. package/dist/cli/server.js +274 -0
  25. package/dist/content/content-layer.js +63 -3
  26. package/dist/content/loaders/file.js +1 -1
  27. package/dist/content/loaders/glob.js +3 -2
  28. package/dist/content/mutable-data-store.js +3 -3
  29. package/dist/content/runtime.d.ts +3 -0
  30. package/dist/content/runtime.js +8 -2
  31. package/dist/content/types-generator.js +1 -0
  32. package/dist/content/vite-plugin-content-assets.js +6 -1
  33. package/dist/content/vite-plugin-content-imports.js +12 -3
  34. package/dist/content/vite-plugin-content-virtual-mod.js +28 -16
  35. package/dist/core/app/entrypoints/index.d.ts +2 -0
  36. package/dist/core/app/entrypoints/index.js +14 -0
  37. package/dist/core/app/types.d.ts +2 -0
  38. package/dist/core/build/config-hash/index.d.ts +8 -0
  39. package/dist/core/build/config-hash/index.js +23 -0
  40. package/dist/core/build/config-hash/input.d.ts +199 -0
  41. package/dist/core/build/config-hash/input.js +43 -0
  42. package/dist/core/build/generate.d.ts +3 -1
  43. package/dist/core/build/generate.js +149 -14
  44. package/dist/core/build/incremental-content-collector.d.ts +10 -0
  45. package/dist/core/build/incremental-content-collector.js +33 -0
  46. package/dist/core/build/incremental-image-collector.d.ts +11 -0
  47. package/dist/core/build/incremental-image-collector.js +32 -0
  48. package/dist/core/build/incremental-metadata.d.ts +9 -0
  49. package/dist/core/build/incremental-metadata.js +17 -0
  50. package/dist/core/build/incremental.d.ts +123 -0
  51. package/dist/core/build/incremental.js +178 -0
  52. package/dist/core/build/index.d.ts +3 -0
  53. package/dist/core/build/index.js +6 -2
  54. package/dist/core/build/internal.d.ts +19 -0
  55. package/dist/core/build/lockfile/finder.d.ts +22 -0
  56. package/dist/core/build/lockfile/finder.js +45 -0
  57. package/dist/core/build/lockfile/hasher.d.ts +16 -0
  58. package/dist/core/build/lockfile/hasher.js +46 -0
  59. package/dist/core/build/lockfile/index.d.ts +9 -0
  60. package/dist/core/build/lockfile/index.js +16 -0
  61. package/dist/core/build/plugins/index.js +3 -1
  62. package/dist/core/build/plugins/plugin-incremental.d.ts +14 -0
  63. package/dist/core/build/plugins/plugin-incremental.js +152 -0
  64. package/dist/core/build/plugins/plugin-manifest.js +11 -2
  65. package/dist/core/build/types.d.ts +2 -0
  66. package/dist/core/config/schemas/base.d.ts +3 -2
  67. package/dist/core/config/schemas/base.js +1 -0
  68. package/dist/core/config/schemas/defaults.d.ts +1 -0
  69. package/dist/core/config/schemas/defaults.js +1 -0
  70. package/dist/core/config/schemas/relative.d.ts +7 -4
  71. package/dist/core/config/settings.js +1 -1
  72. package/dist/core/constants.js +1 -1
  73. package/dist/core/create-vite.js +2 -1
  74. package/dist/core/dev/dev.js +1 -1
  75. package/dist/core/dev/lockfile.d.ts +8 -7
  76. package/dist/core/dev/lockfile.js +13 -13
  77. package/dist/core/encryption.d.ts +6 -0
  78. package/dist/core/encryption.js +14 -2
  79. package/dist/core/errors/default-handler.js +4 -5
  80. package/dist/core/fetch/fetch-state.js +1 -0
  81. package/dist/core/fetch/index.js +1 -1
  82. package/dist/core/logger/config.d.ts +1 -1
  83. package/dist/core/logger/load.d.ts +3 -1
  84. package/dist/core/logger/load.js +3 -3
  85. package/dist/core/logger/utils.d.ts +6 -4
  86. package/dist/core/logger/utils.js +13 -5
  87. package/dist/core/logger/vite-plugin.js +1 -1
  88. package/dist/core/messages/runtime.js +1 -1
  89. package/dist/core/middleware/vite-plugin.d.ts +1 -1
  90. package/dist/core/middleware/vite-plugin.js +11 -24
  91. package/dist/core/preview/static-preview-server.d.ts +2 -0
  92. package/dist/core/preview/static-preview-server.js +1 -0
  93. package/dist/core/routing/dev.d.ts +3 -1
  94. package/dist/core/routing/dev.js +10 -2
  95. package/dist/core/routing/handler.js +1 -1
  96. package/dist/core/session/config.d.ts +2 -2
  97. package/dist/core/session/config.js +2 -1
  98. package/dist/core/session/provider-disabled.d.ts +2 -0
  99. package/dist/core/session/provider-disabled.js +7 -0
  100. package/dist/core/session/provider.d.ts +1 -0
  101. package/dist/core/session/provider.js +4 -0
  102. package/dist/core/session/utils.js +3 -0
  103. package/dist/core/session/vite-plugin.d.ts +3 -0
  104. package/dist/core/session/vite-plugin.js +41 -6
  105. package/dist/integrations/hooks.js +3 -1
  106. package/dist/prefetch/index.js +18 -1
  107. package/dist/prefetch/speculation-rules.d.ts +6 -0
  108. package/dist/prefetch/speculation-rules.js +22 -0
  109. package/dist/runtime/prerender/static-paths.js +5 -1
  110. package/dist/runtime/server/render/head.js +10 -0
  111. package/dist/runtime/server/render/util.js +3 -0
  112. package/dist/transitions/swap-functions.js +49 -4
  113. package/dist/types/public/common.d.ts +1 -0
  114. package/dist/types/public/config.d.ts +81 -9
  115. package/dist/types/public/integrations.d.ts +29 -2
  116. package/dist/types/public/internal.d.ts +2 -0
  117. package/dist/types/public/preview.d.ts +2 -0
  118. package/dist/vite-plugin-app/app.d.ts +3 -1
  119. package/dist/vite-plugin-app/app.js +4 -3
  120. package/dist/vite-plugin-config-alias/index.js +1 -0
  121. package/dist/vite-plugin-environment/index.js +4 -1
  122. package/package.json +1 -2
@@ -0,0 +1,178 @@
1
+ import fs from "node:fs";
2
+ const INCREMENTAL_CACHE_FILE = "incremental-build.json";
3
+ const INCREMENTAL_OUTPUT_DIR = "dist/";
4
+ const CACHE_VERSION = 1;
5
+ function getManifestFile(settings) {
6
+ return new URL(INCREMENTAL_CACHE_FILE, settings.config.cacheDir);
7
+ }
8
+ function getCachedOutputFile(settings, outputFile) {
9
+ return new URL(outputFile, new URL(INCREMENTAL_OUTPUT_DIR, settings.config.cacheDir));
10
+ }
11
+ function readManifest(settings, expectedConfigHash, expectedLockfileHash) {
12
+ try {
13
+ const raw = fs.readFileSync(getManifestFile(settings), "utf-8");
14
+ const data = JSON.parse(raw);
15
+ if (data.version !== CACHE_VERSION) {
16
+ return null;
17
+ }
18
+ if (data.configHash !== expectedConfigHash) {
19
+ return null;
20
+ }
21
+ if (data.lockfileHash !== expectedLockfileHash) {
22
+ return null;
23
+ }
24
+ return data;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ class IncrementalBuildCache {
30
+ #previous;
31
+ #next;
32
+ #contentEntryHashes;
33
+ #createdDirs = /* @__PURE__ */ new Set();
34
+ constructor(configHash, lockfileHash, keyDigest, contentEntryHashes = /* @__PURE__ */ new Map(), previous = null) {
35
+ this.#previous = previous;
36
+ this.#contentEntryHashes = contentEntryHashes;
37
+ this.#next = { version: CACHE_VERSION, configHash, lockfileHash, keyDigest, routes: {} };
38
+ }
39
+ /**
40
+ * Load the cache from disk. When no valid manifest exists (missing, wrong
41
+ * version, or a config or lockfile hash mismatch) the returned cache has no
42
+ * previous build, so every path is rendered as a full build.
43
+ *
44
+ * `contentEntryHashes` is this build's map of content-entry render hashes,
45
+ * used to detect when the content a path renders has changed.
46
+ *
47
+ * `force` ignores any existing manifest so every path is rebuilt, while still
48
+ * recording a fresh cache for the next build.
49
+ */
50
+ static load(settings, configHash, lockfileHash, keyDigest, contentEntryHashes = /* @__PURE__ */ new Map(), force = false) {
51
+ return new IncrementalBuildCache(
52
+ configHash,
53
+ lockfileHash,
54
+ keyDigest,
55
+ contentEntryHashes,
56
+ force ? null : readManifest(settings, configHash, lockfileHash)
57
+ );
58
+ }
59
+ /**
60
+ * Determine if a path can be reused from the previous build. A path is
61
+ * skippable when:
62
+ * 1. It returned a cacheKey in this build.
63
+ * 2. The previous cache has an entry for the route.
64
+ * 3. The route's dependency hash matches the previous build (template code is identical).
65
+ * 4. The previous cache has an entry for this exact path.
66
+ * 5. The path's cacheKey matches the previous build (user data is identical).
67
+ * 6. Every content entry the path rendered last build still has a matching
68
+ * render hash (imported components inside that content are unchanged).
69
+ * 7. If the path renders a server island, the encryption key is unchanged, so
70
+ * the ciphertext baked into the restored HTML is still decryptable.
71
+ */
72
+ canSkip(routeComponent, pathname, dependencyHash, cacheKey, hasServerIsland = false) {
73
+ const routeEntry = this.#previous?.routes[routeComponent];
74
+ if (!routeEntry) return false;
75
+ if (hasServerIsland && this.#previous?.keyDigest !== this.#next.keyDigest) return false;
76
+ if (routeEntry.dependencyHash !== dependencyHash) return false;
77
+ const pathEntry = routeEntry.paths[pathname];
78
+ if (!pathEntry) return false;
79
+ if (pathEntry.cacheKey !== cacheKey) return false;
80
+ if (pathEntry.contentHashes) {
81
+ for (const [entryPath, previousHash] of Object.entries(pathEntry.contentHashes)) {
82
+ if (this.#contentEntryHashes.get(entryPath) !== previousHash) return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+ /**
88
+ * The content entries a path rendered in the previous build, so a skipped path
89
+ * can carry its content-entry tracking forward without re-rendering.
90
+ */
91
+ previousContentEntryKeys(routeComponent, pathname) {
92
+ const pathEntry = this.#previous?.routes[routeComponent]?.paths[pathname];
93
+ return pathEntry?.contentHashes ? Object.keys(pathEntry.contentHashes) : void 0;
94
+ }
95
+ /**
96
+ * The image transforms a path resolved in the previous build, so a skipped
97
+ * path can replay them and carry them forward without re-rendering.
98
+ */
99
+ previousStaticImages(routeComponent, pathname) {
100
+ return this.#previous?.routes[routeComponent]?.paths[pathname]?.staticImages;
101
+ }
102
+ /**
103
+ * The response headers a path collected in the previous build, so a skipped
104
+ * path can replay them into a `staticHeaders` adapter's headers file.
105
+ */
106
+ previousHeaders(routeComponent, pathname) {
107
+ return this.#previous?.routes[routeComponent]?.paths[pathname]?.headers;
108
+ }
109
+ /** Record a path in the next manifest so a later build can skip or prune it. */
110
+ record(routeComponent, dependencyHash, pathname, cacheKey, outputFile, contentEntryKeys, staticImages, headers) {
111
+ let routeEntry = this.#next.routes[routeComponent];
112
+ if (!routeEntry) {
113
+ routeEntry = { dependencyHash, paths: {} };
114
+ this.#next.routes[routeComponent] = routeEntry;
115
+ }
116
+ routeEntry.dependencyHash = dependencyHash;
117
+ const pathEntry = { cacheKey, outputFile };
118
+ if (contentEntryKeys && contentEntryKeys.length > 0) {
119
+ const contentHashes = {};
120
+ for (const key of contentEntryKeys) {
121
+ const hash = this.#contentEntryHashes.get(key);
122
+ if (hash !== void 0) contentHashes[key] = hash;
123
+ }
124
+ if (Object.keys(contentHashes).length > 0) pathEntry.contentHashes = contentHashes;
125
+ }
126
+ if (staticImages && staticImages.length > 0) pathEntry.staticImages = staticImages;
127
+ if (headers && headers.length > 0) pathEntry.headers = headers;
128
+ routeEntry.paths[pathname] = pathEntry;
129
+ }
130
+ /**
131
+ * Cache copies recorded in the previous build that are no longer keyed in this
132
+ * one, either because the path was removed from `getStaticPaths()` or dropped
133
+ * its `cacheKey`. Their stored copies are stale and should be pruned. Paths
134
+ * that are still keyed keep their copies, even when the `cacheKey` changed.
135
+ */
136
+ findOrphanedFiles() {
137
+ if (!this.#previous) return [];
138
+ const orphans = [];
139
+ for (const [routeComponent, routeEntry] of Object.entries(this.#previous.routes)) {
140
+ const nextRouteEntry = this.#next.routes[routeComponent];
141
+ for (const [pathname, pathEntry] of Object.entries(routeEntry.paths)) {
142
+ if (!nextRouteEntry?.paths[pathname]) {
143
+ orphans.push(pathEntry.outputFile);
144
+ }
145
+ }
146
+ }
147
+ return orphans;
148
+ }
149
+ writeManifest(settings) {
150
+ const manifestFile = getManifestFile(settings);
151
+ fs.mkdirSync(new URL("./", manifestFile), { recursive: true });
152
+ fs.writeFileSync(manifestFile, JSON.stringify(this.#next, null, " "));
153
+ }
154
+ async restoreOutputFile(settings, outputFile, destination) {
155
+ const cachedOutputFile = getCachedOutputFile(settings, outputFile);
156
+ if (!fs.existsSync(cachedOutputFile)) return false;
157
+ await this.#ensureDir(new URL("./", destination));
158
+ await fs.promises.copyFile(cachedOutputFile, destination);
159
+ return true;
160
+ }
161
+ async writeOutputFile(settings, outputFile, body) {
162
+ const cachedOutputFile = getCachedOutputFile(settings, outputFile);
163
+ await this.#ensureDir(new URL("./", cachedOutputFile));
164
+ await fs.promises.writeFile(cachedOutputFile, body);
165
+ }
166
+ async deleteOutputFile(settings, outputFile) {
167
+ await fs.promises.rm(getCachedOutputFile(settings, outputFile), { force: true });
168
+ }
169
+ async #ensureDir(dir) {
170
+ const key = dir.href;
171
+ if (this.#createdDirs.has(key)) return;
172
+ await fs.promises.mkdir(dir, { recursive: true });
173
+ this.#createdDirs.add(key);
174
+ }
175
+ }
176
+ export {
177
+ IncrementalBuildCache
178
+ };
@@ -31,6 +31,8 @@ interface AstroBuilderOptions extends BuildOptions {
31
31
  * Set to false for in-memory builds that don't need type generation.
32
32
  */
33
33
  sync?: boolean;
34
+ /** Set by `astro build --force` to rebuild every page and ignore the incremental cache. */
35
+ force?: boolean;
34
36
  }
35
37
  export declare class AstroBuilder {
36
38
  private settings;
@@ -41,6 +43,7 @@ export declare class AstroBuilder {
41
43
  private routesList;
42
44
  private timer;
43
45
  private sync;
46
+ private force;
44
47
  constructor(settings: AstroSettings, options: AstroBuilderOptions);
45
48
  /** Setup Vite and run any async setup logic that couldn't run inside of the constructor. */
46
49
  private setup;
@@ -44,7 +44,8 @@ async function build(inlineConfig, options = {}) {
44
44
  ...options,
45
45
  logger,
46
46
  mode: inlineConfig.mode ?? "production",
47
- runtimeMode: options.devOutput ? "development" : "production"
47
+ runtimeMode: options.devOutput ? "development" : "production",
48
+ force: inlineConfig.force ?? false
48
49
  });
49
50
  await builder.run();
50
51
  }
@@ -57,12 +58,14 @@ class AstroBuilder {
57
58
  routesList;
58
59
  timer;
59
60
  sync;
61
+ force;
60
62
  constructor(settings, options) {
61
63
  this.mode = options.mode;
62
64
  this.runtimeMode = options.runtimeMode;
63
65
  this.settings = settings;
64
66
  this.logger = options.logger;
65
67
  this.sync = options.sync ?? true;
68
+ this.force = options.force ?? false;
66
69
  this.origin = settings.config.site ? new URL(settings.config.site).origin : `http://localhost:${settings.config.server.port}`;
67
70
  this.routesList = options.routesList ?? { routes: [] };
68
71
  this.timer = {};
@@ -151,7 +154,8 @@ class AstroBuilder {
151
154
  origin: this.origin,
152
155
  pageNames,
153
156
  viteConfig,
154
- key: keyPromise
157
+ key: keyPromise,
158
+ force: this.force
155
159
  };
156
160
  await viteBuild(opts);
157
161
  this.timer.assetsStart = performance.now();
@@ -107,6 +107,25 @@ export interface BuildInternals {
107
107
  * vs CSS that was included in SSR.
108
108
  */
109
109
  ssrRenderedExports?: Map<string, Set<string>>;
110
+ /**
111
+ * Map of page component path -> dependency hash for incremental builds.
112
+ * Populated during the prerender Rolldown build by the incremental plugin.
113
+ */
114
+ pageDependencyHashes?: Map<string, string>;
115
+ /**
116
+ * Map of content entry root-relative `filePath` -> render-graph hash for
117
+ * incremental builds. Keyed to match what the content runtime reports when it
118
+ * renders an entry, so a path's cache entry can be invalidated when a component
119
+ * imported by the content it renders changes.
120
+ */
121
+ contentEntryRenderHashes?: Map<string, string>;
122
+ /**
123
+ * Set of page component paths whose render graph contains a server island.
124
+ * Populated during the prerender Rolldown build by the incremental plugin.
125
+ * Such pages bake key-bound ciphertext into their HTML, so the incremental
126
+ * cache only reuses them while the encryption key is unchanged.
127
+ */
128
+ serverIslandPageComponents?: Set<string>;
110
129
  }
111
130
  /**
112
131
  * Creates internal maps used to coordinate the CSS and HTML plugins.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Lockfile names recognized across the package managers we support. A project
3
+ * can commit more than one, so these carry no priority: every match in a
4
+ * directory is returned.
5
+ */
6
+ export declare const KNOWN_LOCKFILES: readonly ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"];
7
+ /** Subset of `node:fs` the finder depends on, so tests can pass a fake. */
8
+ export interface LockfileFinderFs {
9
+ existsSync(path: string): boolean;
10
+ }
11
+ export declare class LockfileFinder {
12
+ #private;
13
+ constructor(fs?: LockfileFinderFs, lockfileNames?: readonly string[]);
14
+ /**
15
+ * Walk upward from `startDir` and return the absolute paths of every known
16
+ * lockfile in the first directory that contains at least one. In a workspace
17
+ * the lockfile lives above the individual project, so the walk continues until
18
+ * a directory yields a match. Returns an empty array if the filesystem root is
19
+ * reached without finding one.
20
+ */
21
+ find(startDir: string): string[];
22
+ }
@@ -0,0 +1,45 @@
1
+ import nodeFs from "node:fs";
2
+ import path from "node:path";
3
+ const KNOWN_LOCKFILES = [
4
+ "pnpm-lock.yaml",
5
+ "package-lock.json",
6
+ "yarn.lock",
7
+ "bun.lock",
8
+ "bun.lockb"
9
+ ];
10
+ class LockfileFinder {
11
+ #fs;
12
+ #lockfileNames;
13
+ constructor(fs = nodeFs, lockfileNames = KNOWN_LOCKFILES) {
14
+ this.#fs = fs;
15
+ this.#lockfileNames = lockfileNames;
16
+ }
17
+ /**
18
+ * Walk upward from `startDir` and return the absolute paths of every known
19
+ * lockfile in the first directory that contains at least one. In a workspace
20
+ * the lockfile lives above the individual project, so the walk continues until
21
+ * a directory yields a match. Returns an empty array if the filesystem root is
22
+ * reached without finding one.
23
+ */
24
+ find(startDir) {
25
+ let dir = path.resolve(startDir);
26
+ let previous = "";
27
+ while (dir !== previous) {
28
+ const found = [];
29
+ for (const name of this.#lockfileNames) {
30
+ const candidate = path.join(dir, name);
31
+ if (this.#fs.existsSync(candidate)) {
32
+ found.push(candidate);
33
+ }
34
+ }
35
+ if (found.length > 0) return found;
36
+ previous = dir;
37
+ dir = path.dirname(dir);
38
+ }
39
+ return [];
40
+ }
41
+ }
42
+ export {
43
+ KNOWN_LOCKFILES,
44
+ LockfileFinder
45
+ };
@@ -0,0 +1,16 @@
1
+ /** Subset of `node:fs` the hasher depends on, so tests can pass a fake. */
2
+ export interface LockfileHasherFs {
3
+ readFileSync(path: string): Buffer;
4
+ }
5
+ export declare class LockfileHasher {
6
+ #private;
7
+ constructor(fs?: LockfileHasherFs);
8
+ /**
9
+ * Produce a sha256 over the raw bytes of the given lockfiles. Files are sorted
10
+ * by name so discovery order does not affect the result, and each name is mixed
11
+ * in so identical content under different package managers still yields distinct
12
+ * hashes. Returns an empty string for an empty list, letting callers treat
13
+ * "no lockfile" as "no signal" rather than a hash collision.
14
+ */
15
+ hash(lockfilePaths: string[]): Promise<string>;
16
+ }
@@ -0,0 +1,46 @@
1
+ import nodeFs from "node:fs";
2
+ import path from "node:path";
3
+ import { encodeHexLowerCase } from "@oslojs/encoding";
4
+ const encoder = new TextEncoder();
5
+ const SEPARATOR = new Uint8Array([0]);
6
+ function concatBytes(parts) {
7
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
8
+ const out = new Uint8Array(total);
9
+ let offset = 0;
10
+ for (const part of parts) {
11
+ out.set(part, offset);
12
+ offset += part.length;
13
+ }
14
+ return out;
15
+ }
16
+ class LockfileHasher {
17
+ #fs;
18
+ constructor(fs = nodeFs) {
19
+ this.#fs = fs;
20
+ }
21
+ /**
22
+ * Produce a sha256 over the raw bytes of the given lockfiles. Files are sorted
23
+ * by name so discovery order does not affect the result, and each name is mixed
24
+ * in so identical content under different package managers still yields distinct
25
+ * hashes. Returns an empty string for an empty list, letting callers treat
26
+ * "no lockfile" as "no signal" rather than a hash collision.
27
+ */
28
+ async hash(lockfilePaths) {
29
+ if (lockfilePaths.length === 0) return "";
30
+ const sorted = [...lockfilePaths].sort(
31
+ (a, b) => path.basename(a).localeCompare(path.basename(b))
32
+ );
33
+ const parts = [];
34
+ for (const filePath of sorted) {
35
+ parts.push(encoder.encode(path.basename(filePath)));
36
+ parts.push(SEPARATOR);
37
+ parts.push(new Uint8Array(this.#fs.readFileSync(filePath)));
38
+ parts.push(SEPARATOR);
39
+ }
40
+ const digest = await crypto.subtle.digest("SHA-256", concatBytes(parts));
41
+ return encodeHexLowerCase(new Uint8Array(digest));
42
+ }
43
+ }
44
+ export {
45
+ LockfileHasher
46
+ };
@@ -0,0 +1,9 @@
1
+ import nodeFs from 'node:fs';
2
+ export { KNOWN_LOCKFILES, LockfileFinder, type LockfileFinderFs } from './finder.js';
3
+ export { LockfileHasher, type LockfileHasherFs } from './hasher.js';
4
+ /**
5
+ * Find the nearest lockfiles at or above `startDir` and hash them into a single
6
+ * digest used to globally invalidate the incremental build cache when
7
+ * dependencies change. Returns an empty string when no lockfile is found.
8
+ */
9
+ export declare function computeLockfileHash(startDir: string, fs?: typeof nodeFs): Promise<string>;
@@ -0,0 +1,16 @@
1
+ import nodeFs from "node:fs";
2
+ import { LockfileFinder } from "./finder.js";
3
+ import { LockfileHasher } from "./hasher.js";
4
+ import { KNOWN_LOCKFILES, LockfileFinder as LockfileFinder2 } from "./finder.js";
5
+ import { LockfileHasher as LockfileHasher2 } from "./hasher.js";
6
+ function computeLockfileHash(startDir, fs = nodeFs) {
7
+ const finder = new LockfileFinder(fs);
8
+ const hasher = new LockfileHasher(fs);
9
+ return hasher.hash(finder.find(startDir));
10
+ }
11
+ export {
12
+ KNOWN_LOCKFILES,
13
+ LockfileFinder2 as LockfileFinder,
14
+ LockfileHasher2 as LockfileHasher,
15
+ computeLockfileHash
16
+ };
@@ -10,6 +10,7 @@ import { pluginPrerender } from "./plugin-prerender.js";
10
10
  import { pluginScripts } from "./plugin-scripts.js";
11
11
  import { pluginSSR } from "./plugin-ssr.js";
12
12
  import { pluginChunkImports } from "./plugin-chunk-imports.js";
13
+ import { pluginIncremental } from "./plugin-incremental.js";
13
14
  import { pluginNoop } from "./plugin-noop.js";
14
15
  import { vitePluginSSRAssets } from "../vite-plugin-ssr-assets.js";
15
16
  function getAllBuildPlugins(internals, options) {
@@ -27,7 +28,8 @@ function getAllBuildPlugins(internals, options) {
27
28
  ...pluginSSR(options, internals),
28
29
  pluginNoop(),
29
30
  vitePluginSSRAssets(internals),
30
- pluginChunkImports(options)
31
+ pluginChunkImports(options),
32
+ options.settings.config.experimental.incrementalBuild ? pluginIncremental(internals, options.settings.config.root) : void 0
31
33
  ].filter(Boolean);
32
34
  }
33
35
  export {
@@ -0,0 +1,14 @@
1
+ import type { Plugin as VitePlugin } from 'vite';
2
+ import type { BuildInternals } from '../internal.js';
3
+ /**
4
+ * Captures a dependency hash for each prerendered page route during the build.
5
+ *
6
+ * The base hash is derived during the prerender build from the sorted set of all
7
+ * transitive module dependencies of the page, so any change to the page's
8
+ * template, layouts, components, or utilities produces a different hash. During
9
+ * the client build, the hash of each `client:only` component's and hoisted
10
+ * `<script>`'s own module graph is folded in, since those never enter the
11
+ * prerender graph. Content entries are hashed separately per entry, since their
12
+ * render modules sit behind `content-data`-pruned bridges.
13
+ */
14
+ export declare function pluginIncremental(internals: BuildInternals, root: URL): VitePlugin;
@@ -0,0 +1,152 @@
1
+ import crypto from "node:crypto";
2
+ import { PROPAGATED_ASSET_FLAG } from "../../../content/consts.js";
3
+ import { hasContentFlag } from "../../../content/utils.js";
4
+ import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../../constants.js";
5
+ import { removeQueryString } from "../../path.js";
6
+ import { rootRelativePath } from "../../viteUtils.js";
7
+ import { moduleIsTopLevelPage } from "../graph.js";
8
+ import { isContentDataIncrementalModule } from "../incremental-metadata.js";
9
+ 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
+ const ASSET_PLACEHOLDERS = [
30
+ { token: "__ASTRO_ASSET_IMAGE__", pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g },
31
+ { token: "__VITE_ASSET__", pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }
32
+ ];
33
+ function resolveAssetPlaceholders(graph, code) {
34
+ let resolved = code;
35
+ for (const { token, pattern } of ASSET_PLACEHOLDERS) {
36
+ if (!resolved.includes(token)) continue;
37
+ resolved = resolved.replace(pattern, (placeholder, handle, postfix = "") => {
38
+ try {
39
+ return graph.getFileName(handle) + postfix;
40
+ } catch {
41
+ return placeholder;
42
+ }
43
+ });
44
+ }
45
+ return resolved;
46
+ }
47
+ function hashModules(graph, sortedIds) {
48
+ const hasher = crypto.createHash("sha256");
49
+ for (const id of sortedIds) {
50
+ hasher.update(id);
51
+ hasher.update("\n");
52
+ const code = graph.getModuleInfo(id)?.code;
53
+ if (code != null) {
54
+ hasher.update(resolveAssetPlaceholders(graph, code));
55
+ }
56
+ hasher.update("\n");
57
+ }
58
+ return hasher.digest("hex");
59
+ }
60
+ function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint, hashesByComponent) {
61
+ for (const entrypointId of entrypointIds) {
62
+ const pages = pagesByEntrypoint.get(entrypointId);
63
+ if (!pages?.size) continue;
64
+ const hash = hashModules(graph, collectTransitiveDeps(graph, entrypointId));
65
+ for (const pageData of pages) {
66
+ let list = hashesByComponent.get(pageData.component);
67
+ if (!list) {
68
+ list = [];
69
+ hashesByComponent.set(pageData.component, list);
70
+ }
71
+ list.push(hash);
72
+ }
73
+ }
74
+ }
75
+ function foldClientDependencies(graph, internals) {
76
+ const baseHashes = internals.pageDependencyHashes;
77
+ if (!baseHashes) return;
78
+ const hashesByComponent = /* @__PURE__ */ new Map();
79
+ collectClientEntrypointHashes(
80
+ graph,
81
+ internals.discoveredClientOnlyComponents.keys(),
82
+ internals.pagesByClientOnly,
83
+ hashesByComponent
84
+ );
85
+ collectClientEntrypointHashes(
86
+ graph,
87
+ internals.discoveredScripts,
88
+ internals.pagesByScriptId,
89
+ hashesByComponent
90
+ );
91
+ for (const [component, clientHashes] of hashesByComponent) {
92
+ const hasher = crypto.createHash("sha256");
93
+ hasher.update(baseHashes.get(component) ?? "");
94
+ for (const hash of clientHashes.sort()) {
95
+ hasher.update("\n");
96
+ hasher.update(hash);
97
+ }
98
+ baseHashes.set(component, hasher.digest("hex"));
99
+ }
100
+ }
101
+ function collectContentEntryHashes(graph, root) {
102
+ const entryHashes = /* @__PURE__ */ new Map();
103
+ for (const id of graph.getModuleIds()) {
104
+ if (!hasContentFlag(id, PROPAGATED_ASSET_FLAG)) continue;
105
+ const renderModuleId = removeQueryString(id);
106
+ const key = rootRelativePath(root, renderModuleId, false);
107
+ const deps = collectTransitiveDeps(graph, renderModuleId);
108
+ entryHashes.set(key, hashModules(graph, deps));
109
+ }
110
+ return entryHashes;
111
+ }
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
+ function pluginIncremental(internals, root) {
120
+ return {
121
+ name: "@astro/plugin-incremental",
122
+ applyToEnvironment(environment) {
123
+ return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
124
+ },
125
+ generateBundle() {
126
+ if (this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
127
+ foldClientDependencies(this, internals);
128
+ return;
129
+ }
130
+ const hashes = /* @__PURE__ */ new Map();
131
+ const serverIslandComponents = /* @__PURE__ */ new Set();
132
+ for (const id of this.getModuleIds()) {
133
+ const info = this.getModuleInfo(id);
134
+ if (!info) continue;
135
+ if (!moduleIsTopLevelPage(info)) continue;
136
+ const pageData = getPageDataByViteID(internals, info.id);
137
+ if (!pageData) continue;
138
+ const deps = collectTransitiveDeps(this, info.id);
139
+ hashes.set(pageData.component, hashModules(this, deps));
140
+ if (pageContainsServerIsland(this, deps)) {
141
+ serverIslandComponents.add(pageData.component);
142
+ }
143
+ }
144
+ internals.pageDependencyHashes = hashes;
145
+ internals.contentEntryRenderHashes = collectContentEntryHashes(this, root);
146
+ internals.serverIslandPageComponents = serverIslandComponents;
147
+ }
148
+ };
149
+ }
150
+ export {
151
+ pluginIncremental
152
+ };
@@ -21,7 +21,8 @@ import {
21
21
  trackStyleHashes
22
22
  } from "../../csp/common.js";
23
23
  import { partitionByKind } from "../../csp/runtime.js";
24
- import { encodeKey } from "../../encryption.js";
24
+ import { generateSpeculationRulesContent } from "../../../prefetch/speculation-rules.js";
25
+ import { encodeKey, generateCspDigest } from "../../encryption.js";
25
26
  import { fileExtension, joinPaths, prependForwardSlash } from "../../path.js";
26
27
  import { DEFAULT_COMPONENTS } from "../../routing/default.js";
27
28
  import { getOutFile, getOutFolder } from "../common.js";
@@ -204,6 +205,13 @@ async function buildManifest(opts, internals, staticFiles, encodedKey) {
204
205
  ...settings.injectedCsp.styleHashes,
205
206
  ...await trackStyleHashes(internals, settings, algorithm)
206
207
  ];
208
+ let speculationRulesContent;
209
+ if (settings.config.experimental.clientPrerender && settings.config.prefetch) {
210
+ const prefetchAll = typeof settings.config.prefetch === "object" ? settings.config.prefetch.prefetchAll ?? false : false;
211
+ speculationRulesContent = generateSpeculationRulesContent(prefetchAll);
212
+ const speculationRulesHash = await generateCspDigest(speculationRulesContent, algorithm);
213
+ scriptHashes.push(speculationRulesHash);
214
+ }
207
215
  const scriptDirective = {
208
216
  resources: getScriptResources(cspConfig),
209
217
  hashes: scriptHashes,
@@ -225,7 +233,8 @@ async function buildManifest(opts, internals, staticFiles, encodedKey) {
225
233
  styleHashes: styleDefault.hashes,
226
234
  styleResources: styleDefault.resources,
227
235
  scriptDirective,
228
- styleDirective
236
+ styleDirective,
237
+ speculationRulesContent
229
238
  };
230
239
  }
231
240
  let internalFetchHeaders = void 0;
@@ -38,6 +38,8 @@ export interface StaticBuildOptions {
38
38
  pageNames: string[];
39
39
  viteConfig: InlineConfig;
40
40
  key: Promise<CryptoKey>;
41
+ /** Set by `astro build --force` to rebuild every page and ignore the incremental cache. */
42
+ force: boolean;
41
43
  }
42
44
  type ImportComponentInstance = () => Promise<ComponentInstance>;
43
45
  export interface SinglePageBuiltModule {
@@ -393,7 +393,7 @@ export declare const AstroConfigSchema: z.ZodObject<{
393
393
  }, z.core.$strip>]>>>>>;
394
394
  validateSecrets: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
395
395
  }, z.core.$strict>>>;
396
- session: z.ZodOptional<z.ZodObject<{
396
+ session: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
397
397
  driver: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
398
398
  config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
399
399
  entrypoint: z.ZodUnion<readonly [z.ZodString, z.ZodCustom<URL, URL>]>;
@@ -414,7 +414,7 @@ export declare const AstroConfigSchema: z.ZodObject<{
414
414
  name: string;
415
415
  }, string>>]>>;
416
416
  ttl: z.ZodOptional<z.ZodNumber>;
417
- }, z.core.$strip>>;
417
+ }, z.core.$strip>]>>;
418
418
  prerenderConflictBehavior: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
419
419
  error: "error";
420
420
  ignore: "ignore";
@@ -484,6 +484,7 @@ export declare const AstroConfigSchema: z.ZodObject<{
484
484
  clientPrerender: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
485
485
  contentIntellisense: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
486
486
  chromeDevtoolsWorkspace: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
487
+ incrementalBuild: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
487
488
  svgOptimizer: z.ZodOptional<z.ZodObject<{
488
489
  name: z.ZodString;
489
490
  optimize: z.ZodCustom<(contents: string) => string | Promise<string>, (contents: string) => string | Promise<string>>;