astro 7.1.5 → 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.
- package/dist/assets/build/generate.d.ts +8 -1
- package/dist/assets/build/generate.js +21 -1
- package/dist/assets/fonts/providers/index.js +12 -2
- package/dist/assets/types.d.ts +13 -0
- package/dist/assets/vite-plugin-assets.js +8 -0
- package/dist/cli/add/index.js +1 -1
- package/dist/cli/agent.d.ts +1 -0
- package/dist/cli/agent.js +11 -0
- package/dist/cli/dev/background.d.ts +3 -16
- package/dist/cli/dev/background.js +7 -93
- package/dist/cli/dev/index.d.ts +12 -0
- package/dist/cli/dev/index.js +30 -23
- package/dist/cli/dev/logs.js +2 -64
- package/dist/cli/dev/status.d.ts +2 -9
- package/dist/cli/dev/status.js +6 -24
- package/dist/cli/dev/stop.d.ts +2 -6
- package/dist/cli/dev/stop.js +6 -14
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/cli/install-package.js +1 -1
- package/dist/cli/preview/index.js +77 -3
- package/dist/cli/server.d.ts +60 -0
- package/dist/cli/server.js +274 -0
- package/dist/content/consts.d.ts +4 -0
- package/dist/content/consts.js +8 -0
- package/dist/content/content-layer.js +3 -3
- package/dist/content/data-store-writer.d.ts +1 -1
- package/dist/content/data-store-writer.js +4 -3
- package/dist/content/data-store.d.ts +3 -3
- package/dist/content/data-store.js +3 -3
- package/dist/content/mutable-data-store.d.ts +3 -3
- package/dist/content/mutable-data-store.js +5 -5
- package/dist/content/paths.d.ts +2 -1
- package/dist/content/paths.js +11 -0
- package/dist/content/runtime.d.ts +2 -0
- package/dist/content/runtime.js +2 -0
- package/dist/content/types-generator.js +1 -0
- package/dist/content/vite-plugin-content-assets.js +6 -1
- package/dist/content/vite-plugin-content-imports.js +12 -3
- package/dist/content/vite-plugin-content-virtual-mod.js +69 -31
- package/dist/core/app/entrypoints/index.d.ts +2 -0
- package/dist/core/app/entrypoints/index.js +14 -0
- package/dist/core/build/config-hash/index.d.ts +8 -0
- package/dist/core/build/config-hash/index.js +23 -0
- package/dist/core/build/config-hash/input.d.ts +199 -0
- package/dist/core/build/config-hash/input.js +43 -0
- package/dist/core/build/generate.d.ts +3 -1
- package/dist/core/build/generate.js +204 -62
- package/dist/core/build/incremental-content-collector.d.ts +10 -0
- package/dist/core/build/incremental-content-collector.js +33 -0
- package/dist/core/build/incremental-image-collector.d.ts +11 -0
- package/dist/core/build/incremental-image-collector.js +32 -0
- package/dist/core/build/incremental-metadata.d.ts +9 -0
- package/dist/core/build/incremental-metadata.js +17 -0
- package/dist/core/build/incremental.d.ts +123 -0
- package/dist/core/build/incremental.js +178 -0
- package/dist/core/build/index.d.ts +3 -0
- package/dist/core/build/index.js +6 -2
- package/dist/core/build/internal.d.ts +19 -0
- package/dist/core/build/lockfile/finder.d.ts +22 -0
- package/dist/core/build/lockfile/finder.js +45 -0
- package/dist/core/build/lockfile/hasher.d.ts +16 -0
- package/dist/core/build/lockfile/hasher.js +46 -0
- package/dist/core/build/lockfile/index.d.ts +9 -0
- package/dist/core/build/lockfile/index.js +16 -0
- package/dist/core/build/plugins/index.js +3 -1
- package/dist/core/build/plugins/plugin-incremental.d.ts +14 -0
- package/dist/core/build/plugins/plugin-incremental.js +134 -0
- package/dist/core/build/types.d.ts +2 -0
- package/dist/core/config/schemas/base.d.ts +7 -6
- package/dist/core/config/schemas/base.js +9 -1
- package/dist/core/config/schemas/defaults.d.ts +1 -0
- package/dist/core/config/schemas/defaults.js +1 -0
- package/dist/core/config/schemas/relative.d.ts +19 -10
- package/dist/core/constants.js +1 -1
- package/dist/core/create-vite.js +2 -1
- package/dist/core/dev/dev.js +5 -4
- package/dist/core/dev/lockfile.d.ts +8 -7
- package/dist/core/dev/lockfile.js +13 -13
- package/dist/core/encryption.d.ts +6 -0
- package/dist/core/encryption.js +14 -2
- package/dist/core/errors/default-handler.js +4 -5
- package/dist/core/fetch/index.js +1 -1
- package/dist/core/logger/config.d.ts +1 -1
- package/dist/core/logger/load.d.ts +3 -1
- package/dist/core/logger/load.js +3 -3
- package/dist/core/logger/utils.d.ts +6 -4
- package/dist/core/logger/utils.js +13 -5
- package/dist/core/logger/vite-plugin.js +1 -1
- package/dist/core/messages/runtime.js +1 -1
- package/dist/core/preview/static-preview-server.d.ts +2 -0
- package/dist/core/preview/static-preview-server.js +1 -0
- package/dist/core/routing/handler.js +1 -1
- package/dist/core/session/config.d.ts +2 -2
- package/dist/core/session/config.js +2 -1
- package/dist/core/session/provider-disabled.d.ts +2 -0
- package/dist/core/session/provider-disabled.js +7 -0
- package/dist/core/session/provider.d.ts +1 -0
- package/dist/core/session/provider.js +4 -0
- package/dist/core/session/utils.js +3 -0
- package/dist/core/session/vite-plugin.d.ts +3 -0
- package/dist/core/session/vite-plugin.js +41 -6
- package/dist/core/sync/index.js +5 -4
- package/dist/integrations/hooks.js +3 -1
- package/dist/runtime/prerender/static-paths.js +5 -1
- package/dist/runtime/server/render/util.js +3 -0
- package/dist/types/public/common.d.ts +1 -0
- package/dist/types/public/config.d.ts +73 -7
- package/dist/types/public/integrations.d.ts +29 -2
- package/dist/types/public/preview.d.ts +2 -0
- package/dist/vite-plugin-config-alias/index.js +1 -0
- package/dist/vite-plugin-environment/index.js +4 -1
- package/dist/vite-plugin-hmr-reload/index.js +60 -18
- package/package.json +2 -3
|
@@ -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;
|
package/dist/core/build/index.js
CHANGED
|
@@ -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,134 @@
|
|
|
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
|
+
function hashModules(graph, sortedIds) {
|
|
30
|
+
const hasher = crypto.createHash("sha256");
|
|
31
|
+
for (const id of sortedIds) {
|
|
32
|
+
hasher.update(id);
|
|
33
|
+
hasher.update("\n");
|
|
34
|
+
const code = graph.getModuleInfo(id)?.code;
|
|
35
|
+
if (code != null) {
|
|
36
|
+
hasher.update(code);
|
|
37
|
+
}
|
|
38
|
+
hasher.update("\n");
|
|
39
|
+
}
|
|
40
|
+
return hasher.digest("hex");
|
|
41
|
+
}
|
|
42
|
+
function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint, hashesByComponent) {
|
|
43
|
+
for (const entrypointId of entrypointIds) {
|
|
44
|
+
const pages = pagesByEntrypoint.get(entrypointId);
|
|
45
|
+
if (!pages?.size) continue;
|
|
46
|
+
const hash = hashModules(graph, collectTransitiveDeps(graph, entrypointId));
|
|
47
|
+
for (const pageData of pages) {
|
|
48
|
+
let list = hashesByComponent.get(pageData.component);
|
|
49
|
+
if (!list) {
|
|
50
|
+
list = [];
|
|
51
|
+
hashesByComponent.set(pageData.component, list);
|
|
52
|
+
}
|
|
53
|
+
list.push(hash);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function foldClientDependencies(graph, internals) {
|
|
58
|
+
const baseHashes = internals.pageDependencyHashes;
|
|
59
|
+
if (!baseHashes) return;
|
|
60
|
+
const hashesByComponent = /* @__PURE__ */ new Map();
|
|
61
|
+
collectClientEntrypointHashes(
|
|
62
|
+
graph,
|
|
63
|
+
internals.discoveredClientOnlyComponents.keys(),
|
|
64
|
+
internals.pagesByClientOnly,
|
|
65
|
+
hashesByComponent
|
|
66
|
+
);
|
|
67
|
+
collectClientEntrypointHashes(
|
|
68
|
+
graph,
|
|
69
|
+
internals.discoveredScripts,
|
|
70
|
+
internals.pagesByScriptId,
|
|
71
|
+
hashesByComponent
|
|
72
|
+
);
|
|
73
|
+
for (const [component, clientHashes] of hashesByComponent) {
|
|
74
|
+
const hasher = crypto.createHash("sha256");
|
|
75
|
+
hasher.update(baseHashes.get(component) ?? "");
|
|
76
|
+
for (const hash of clientHashes.sort()) {
|
|
77
|
+
hasher.update("\n");
|
|
78
|
+
hasher.update(hash);
|
|
79
|
+
}
|
|
80
|
+
baseHashes.set(component, hasher.digest("hex"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function collectContentEntryHashes(graph, root) {
|
|
84
|
+
const entryHashes = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const id of graph.getModuleIds()) {
|
|
86
|
+
if (!hasContentFlag(id, PROPAGATED_ASSET_FLAG)) continue;
|
|
87
|
+
const renderModuleId = removeQueryString(id);
|
|
88
|
+
const key = rootRelativePath(root, renderModuleId, false);
|
|
89
|
+
const deps = collectTransitiveDeps(graph, renderModuleId);
|
|
90
|
+
entryHashes.set(key, hashModules(graph, deps));
|
|
91
|
+
}
|
|
92
|
+
return entryHashes;
|
|
93
|
+
}
|
|
94
|
+
function pageContainsServerIsland(graph, ids) {
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
const serverComponents = graph.getModuleInfo(id)?.meta?.astro?.serverComponents;
|
|
97
|
+
if (serverComponents?.length) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function pluginIncremental(internals, root) {
|
|
102
|
+
return {
|
|
103
|
+
name: "@astro/plugin-incremental",
|
|
104
|
+
applyToEnvironment(environment) {
|
|
105
|
+
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
|
|
106
|
+
},
|
|
107
|
+
generateBundle() {
|
|
108
|
+
if (this.environment?.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
|
|
109
|
+
foldClientDependencies(this, internals);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
113
|
+
const serverIslandComponents = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const id of this.getModuleIds()) {
|
|
115
|
+
const info = this.getModuleInfo(id);
|
|
116
|
+
if (!info) continue;
|
|
117
|
+
if (!moduleIsTopLevelPage(info)) continue;
|
|
118
|
+
const pageData = getPageDataByViteID(internals, info.id);
|
|
119
|
+
if (!pageData) continue;
|
|
120
|
+
const deps = collectTransitiveDeps(this, info.id);
|
|
121
|
+
hashes.set(pageData.component, hashModules(this, deps));
|
|
122
|
+
if (pageContainsServerIsland(this, deps)) {
|
|
123
|
+
serverIslandComponents.add(pageData.component);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
internals.pageDependencyHashes = hashes;
|
|
127
|
+
internals.contentEntryRenderHashes = collectContentEntryHashes(this, root);
|
|
128
|
+
internals.serverIslandPageComponents = serverIslandComponents;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
pluginIncremental
|
|
134
|
+
};
|
|
@@ -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,14 +484,15 @@ 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>>;
|
|
490
491
|
}, z.core.$strip>>;
|
|
491
|
-
collectionStorage: z.ZodDefault<z.ZodOptional<z.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}>>>;
|
|
492
|
+
collectionStorage: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"single-file">, z.ZodLiteral<"chunked">, z.ZodObject<{
|
|
493
|
+
type: z.ZodLiteral<"chunked">;
|
|
494
|
+
chunkSize: z.ZodNumber;
|
|
495
|
+
}, z.core.$strict>]>>>;
|
|
495
496
|
}, z.core.$strict>>;
|
|
496
497
|
legacy: z.ZodPrefault<z.ZodObject<{
|
|
497
498
|
collectionsBackwardsCompat: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -276,8 +276,16 @@ const AstroConfigSchema = z.object({
|
|
|
276
276
|
clientPrerender: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.clientPrerender),
|
|
277
277
|
contentIntellisense: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.contentIntellisense),
|
|
278
278
|
chromeDevtoolsWorkspace: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.chromeDevtoolsWorkspace),
|
|
279
|
+
incrementalBuild: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.incrementalBuild),
|
|
279
280
|
svgOptimizer: SvgOptimizerSchema.optional(),
|
|
280
|
-
collectionStorage: z.
|
|
281
|
+
collectionStorage: z.union([
|
|
282
|
+
z.literal("single-file"),
|
|
283
|
+
z.literal("chunked"),
|
|
284
|
+
z.strictObject({
|
|
285
|
+
type: z.literal("chunked"),
|
|
286
|
+
chunkSize: z.number().int().positive()
|
|
287
|
+
})
|
|
288
|
+
]).optional().default(ASTRO_CONFIG_DEFAULTS.experimental.collectionStorage)
|
|
281
289
|
}).prefault({}),
|
|
282
290
|
legacy: z.object({
|
|
283
291
|
collectionsBackwardsCompat: z.boolean().optional().default(false)
|