astro 7.1.6 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) 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/vite-plugin-assets.js +8 -0
  6. package/dist/cli/add/index.js +1 -1
  7. package/dist/cli/agent.d.ts +1 -0
  8. package/dist/cli/agent.js +11 -0
  9. package/dist/cli/dev/background.d.ts +3 -16
  10. package/dist/cli/dev/background.js +7 -93
  11. package/dist/cli/dev/index.d.ts +12 -0
  12. package/dist/cli/dev/index.js +30 -23
  13. package/dist/cli/dev/logs.js +2 -64
  14. package/dist/cli/dev/status.d.ts +2 -9
  15. package/dist/cli/dev/status.js +6 -24
  16. package/dist/cli/dev/stop.d.ts +2 -6
  17. package/dist/cli/dev/stop.js +6 -14
  18. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  19. package/dist/cli/install-package.js +1 -1
  20. package/dist/cli/preview/index.js +77 -3
  21. package/dist/cli/server.d.ts +60 -0
  22. package/dist/cli/server.js +274 -0
  23. package/dist/content/content-layer.js +3 -3
  24. package/dist/content/runtime.d.ts +2 -0
  25. package/dist/content/runtime.js +2 -0
  26. package/dist/content/types-generator.js +1 -0
  27. package/dist/content/vite-plugin-content-assets.js +6 -1
  28. package/dist/content/vite-plugin-content-imports.js +12 -3
  29. package/dist/content/vite-plugin-content-virtual-mod.js +28 -16
  30. package/dist/core/app/entrypoints/index.d.ts +2 -0
  31. package/dist/core/app/entrypoints/index.js +14 -0
  32. package/dist/core/build/config-hash/index.d.ts +8 -0
  33. package/dist/core/build/config-hash/index.js +23 -0
  34. package/dist/core/build/config-hash/input.d.ts +199 -0
  35. package/dist/core/build/config-hash/input.js +43 -0
  36. package/dist/core/build/generate.d.ts +3 -1
  37. package/dist/core/build/generate.js +149 -14
  38. package/dist/core/build/incremental-content-collector.d.ts +10 -0
  39. package/dist/core/build/incremental-content-collector.js +33 -0
  40. package/dist/core/build/incremental-image-collector.d.ts +11 -0
  41. package/dist/core/build/incremental-image-collector.js +32 -0
  42. package/dist/core/build/incremental-metadata.d.ts +9 -0
  43. package/dist/core/build/incremental-metadata.js +17 -0
  44. package/dist/core/build/incremental.d.ts +123 -0
  45. package/dist/core/build/incremental.js +178 -0
  46. package/dist/core/build/index.d.ts +3 -0
  47. package/dist/core/build/index.js +6 -2
  48. package/dist/core/build/internal.d.ts +19 -0
  49. package/dist/core/build/lockfile/finder.d.ts +22 -0
  50. package/dist/core/build/lockfile/finder.js +45 -0
  51. package/dist/core/build/lockfile/hasher.d.ts +16 -0
  52. package/dist/core/build/lockfile/hasher.js +46 -0
  53. package/dist/core/build/lockfile/index.d.ts +9 -0
  54. package/dist/core/build/lockfile/index.js +16 -0
  55. package/dist/core/build/plugins/index.js +3 -1
  56. package/dist/core/build/plugins/plugin-incremental.d.ts +14 -0
  57. package/dist/core/build/plugins/plugin-incremental.js +134 -0
  58. package/dist/core/build/types.d.ts +2 -0
  59. package/dist/core/config/schemas/base.d.ts +3 -2
  60. package/dist/core/config/schemas/base.js +1 -0
  61. package/dist/core/config/schemas/defaults.d.ts +1 -0
  62. package/dist/core/config/schemas/defaults.js +1 -0
  63. package/dist/core/config/schemas/relative.d.ts +7 -4
  64. package/dist/core/constants.js +1 -1
  65. package/dist/core/create-vite.js +2 -1
  66. package/dist/core/dev/dev.js +1 -1
  67. package/dist/core/dev/lockfile.d.ts +8 -7
  68. package/dist/core/dev/lockfile.js +13 -13
  69. package/dist/core/encryption.d.ts +6 -0
  70. package/dist/core/encryption.js +14 -2
  71. package/dist/core/errors/default-handler.js +4 -5
  72. package/dist/core/fetch/index.js +1 -1
  73. package/dist/core/logger/config.d.ts +1 -1
  74. package/dist/core/logger/load.d.ts +3 -1
  75. package/dist/core/logger/load.js +3 -3
  76. package/dist/core/logger/utils.d.ts +6 -4
  77. package/dist/core/logger/utils.js +13 -5
  78. package/dist/core/logger/vite-plugin.js +1 -1
  79. package/dist/core/messages/runtime.js +1 -1
  80. package/dist/core/preview/static-preview-server.d.ts +2 -0
  81. package/dist/core/preview/static-preview-server.js +1 -0
  82. package/dist/core/routing/handler.js +1 -1
  83. package/dist/core/session/config.d.ts +2 -2
  84. package/dist/core/session/config.js +2 -1
  85. package/dist/core/session/provider-disabled.d.ts +2 -0
  86. package/dist/core/session/provider-disabled.js +7 -0
  87. package/dist/core/session/provider.d.ts +1 -0
  88. package/dist/core/session/provider.js +4 -0
  89. package/dist/core/session/utils.js +3 -0
  90. package/dist/core/session/vite-plugin.d.ts +3 -0
  91. package/dist/core/session/vite-plugin.js +41 -6
  92. package/dist/integrations/hooks.js +3 -1
  93. package/dist/runtime/prerender/static-paths.js +5 -1
  94. package/dist/runtime/server/render/util.js +3 -0
  95. package/dist/types/public/common.d.ts +1 -0
  96. package/dist/types/public/config.d.ts +59 -2
  97. package/dist/types/public/integrations.d.ts +29 -2
  98. package/dist/types/public/preview.d.ts +2 -0
  99. package/dist/vite-plugin-config-alias/index.js +1 -0
  100. package/dist/vite-plugin-environment/index.js +4 -1
  101. package/package.json +1 -2
@@ -0,0 +1,32 @@
1
+ const COLLECTOR_KEY = /* @__PURE__ */ Symbol.for("astro:incremental-static-images");
2
+ function collector() {
3
+ const host = globalThis;
4
+ let value = host[COLLECTOR_KEY];
5
+ if (!value) {
6
+ value = { current: void 0 };
7
+ Object.defineProperty(host, COLLECTOR_KEY, {
8
+ value,
9
+ configurable: false,
10
+ writable: false,
11
+ enumerable: false
12
+ });
13
+ }
14
+ return value;
15
+ }
16
+ function beginImageCollection() {
17
+ collector().current = [];
18
+ }
19
+ function recordStaticImage(image) {
20
+ collector().current?.push(image);
21
+ }
22
+ function endImageCollection() {
23
+ const c = collector();
24
+ const images = c.current;
25
+ c.current = void 0;
26
+ return images;
27
+ }
28
+ export {
29
+ beginImageCollection,
30
+ endImageCollection,
31
+ recordStaticImage
32
+ };
@@ -0,0 +1,9 @@
1
+ export declare const ASTRO_INCREMENTAL_META_KEY = "astroIncremental";
2
+ export declare function createContentDataIncrementalMetadata(): {
3
+ astroIncremental: {
4
+ kind: string;
5
+ };
6
+ };
7
+ export declare function isContentDataIncrementalModule(info: {
8
+ meta?: Record<string, any>;
9
+ } | null | undefined): boolean;
@@ -0,0 +1,17 @@
1
+ const ASTRO_INCREMENTAL_META_KEY = "astroIncremental";
2
+ function createContentDataIncrementalMetadata() {
3
+ return {
4
+ [ASTRO_INCREMENTAL_META_KEY]: {
5
+ kind: "content-data"
6
+ }
7
+ };
8
+ }
9
+ function isContentDataIncrementalModule(info) {
10
+ const metadata = info?.meta?.[ASTRO_INCREMENTAL_META_KEY];
11
+ return metadata?.kind === "content-data";
12
+ }
13
+ export {
14
+ ASTRO_INCREMENTAL_META_KEY,
15
+ createContentDataIncrementalMetadata,
16
+ isContentDataIncrementalModule
17
+ };
@@ -0,0 +1,123 @@
1
+ import type { SerializedStaticImage } from '../../assets/types.js';
2
+ import type { AstroSettings } from '../../types/astro.js';
3
+ export interface IncrementalPathEntry {
4
+ cacheKey: string;
5
+ outputFile: string;
6
+ /**
7
+ * Render-graph hashes of the content entries this path rendered, keyed by the
8
+ * entry's root-relative `filePath`. A change to any of these invalidates the
9
+ * path even when its template and data are unchanged, since content entries
10
+ * render behind `content-data` bridges the per-route hash cannot cross.
11
+ */
12
+ contentHashes?: Record<string, string>;
13
+ /**
14
+ * Optimized-image transforms this path resolved while rendering. Registered
15
+ * transforms are drained into the output after generation, but `dist/` is
16
+ * emptied each build, so a skipped path replays these into the global list to
17
+ * keep the images its restored HTML references from 404ing.
18
+ */
19
+ staticImages?: SerializedStaticImage[];
20
+ /**
21
+ * Response header name/value pairs a `staticHeaders` adapter collected for this
22
+ * path (chiefly the CSP header when delivered as a header rather than a `<meta>`
23
+ * tag). The `astro:build:generated` hook writes these to a static headers file,
24
+ * so a skipped path replays them to keep its route in that file.
25
+ */
26
+ headers?: [string, string][];
27
+ }
28
+ export interface IncrementalRouteEntry {
29
+ dependencyHash: string;
30
+ paths: Record<string, IncrementalPathEntry>;
31
+ }
32
+ /**
33
+ * On-disk shape of the incremental build cache.
34
+ */
35
+ export interface IncrementalManifest {
36
+ version: number;
37
+ /**
38
+ * Hash of the output-affecting subset of the resolved config. A mismatch
39
+ * invalidates the whole cache, since config baked into compiled output or
40
+ * inlined via Vite cannot be seen by the per-route dependency hash.
41
+ */
42
+ configHash: string;
43
+ /**
44
+ * Hash of the project's lockfiles. Externalized dependencies are leaf nodes
45
+ * in the bundle graph with no code and a versionless id, so the per-route
46
+ * dependency hash cannot see when a (possibly transitive) dependency changes.
47
+ * A mismatch invalidates the whole cache.
48
+ */
49
+ lockfileHash: string;
50
+ /**
51
+ * Hash of the server-island encryption key. A page's island props are baked
52
+ * into its HTML as ciphertext bound to this key, so a restored page whose key
53
+ * has changed would be undecryptable at runtime. Server-island pages are only
54
+ * reused when this matches; it does not affect pages without islands.
55
+ */
56
+ keyDigest: string;
57
+ routes: Record<string, IncrementalRouteEntry>;
58
+ }
59
+ /**
60
+ * Tracks which prerendered paths can be reused from a previous build.
61
+ *
62
+ * The invalidation logic (`canSkip`, `record`, `findOrphanedFiles`) is pure and
63
+ * operates on the previous and next manifests held in memory. Disk access is
64
+ * confined to `load` and the output-file methods.
65
+ */
66
+ export declare class IncrementalBuildCache {
67
+ #private;
68
+ constructor(configHash: string, lockfileHash: string, keyDigest: string, contentEntryHashes?: Map<string, string>, previous?: IncrementalManifest | null);
69
+ /**
70
+ * Load the cache from disk. When no valid manifest exists (missing, wrong
71
+ * version, or a config or lockfile hash mismatch) the returned cache has no
72
+ * previous build, so every path is rendered as a full build.
73
+ *
74
+ * `contentEntryHashes` is this build's map of content-entry render hashes,
75
+ * used to detect when the content a path renders has changed.
76
+ *
77
+ * `force` ignores any existing manifest so every path is rebuilt, while still
78
+ * recording a fresh cache for the next build.
79
+ */
80
+ static load(settings: AstroSettings, configHash: string, lockfileHash: string, keyDigest: string, contentEntryHashes?: Map<string, string>, force?: boolean): IncrementalBuildCache;
81
+ /**
82
+ * Determine if a path can be reused from the previous build. A path is
83
+ * skippable when:
84
+ * 1. It returned a cacheKey in this build.
85
+ * 2. The previous cache has an entry for the route.
86
+ * 3. The route's dependency hash matches the previous build (template code is identical).
87
+ * 4. The previous cache has an entry for this exact path.
88
+ * 5. The path's cacheKey matches the previous build (user data is identical).
89
+ * 6. Every content entry the path rendered last build still has a matching
90
+ * render hash (imported components inside that content are unchanged).
91
+ * 7. If the path renders a server island, the encryption key is unchanged, so
92
+ * the ciphertext baked into the restored HTML is still decryptable.
93
+ */
94
+ canSkip(routeComponent: string, pathname: string, dependencyHash: string, cacheKey: string, hasServerIsland?: boolean): boolean;
95
+ /**
96
+ * The content entries a path rendered in the previous build, so a skipped path
97
+ * can carry its content-entry tracking forward without re-rendering.
98
+ */
99
+ previousContentEntryKeys(routeComponent: string, pathname: string): string[] | undefined;
100
+ /**
101
+ * The image transforms a path resolved in the previous build, so a skipped
102
+ * path can replay them and carry them forward without re-rendering.
103
+ */
104
+ previousStaticImages(routeComponent: string, pathname: string): SerializedStaticImage[] | undefined;
105
+ /**
106
+ * The response headers a path collected in the previous build, so a skipped
107
+ * path can replay them into a `staticHeaders` adapter's headers file.
108
+ */
109
+ previousHeaders(routeComponent: string, pathname: string): [string, string][] | undefined;
110
+ /** Record a path in the next manifest so a later build can skip or prune it. */
111
+ record(routeComponent: string, dependencyHash: string, pathname: string, cacheKey: string, outputFile: string, contentEntryKeys?: string[], staticImages?: SerializedStaticImage[], headers?: [string, string][]): void;
112
+ /**
113
+ * Cache copies recorded in the previous build that are no longer keyed in this
114
+ * one, either because the path was removed from `getStaticPaths()` or dropped
115
+ * its `cacheKey`. Their stored copies are stale and should be pruned. Paths
116
+ * that are still keyed keep their copies, even when the `cacheKey` changed.
117
+ */
118
+ findOrphanedFiles(): string[];
119
+ writeManifest(settings: AstroSettings): void;
120
+ restoreOutputFile(settings: AstroSettings, outputFile: string, destination: URL): Promise<boolean>;
121
+ writeOutputFile(settings: AstroSettings, outputFile: string, body: string | Uint8Array): Promise<void>;
122
+ deleteOutputFile(settings: AstroSettings, outputFile: string): Promise<void>;
123
+ }
@@ -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;