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.
- 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/content-layer.js +3 -3
- 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 +28 -16
- 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 +149 -14
- 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 +3 -2
- package/dist/core/config/schemas/base.js +1 -0
- 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 +7 -4
- package/dist/core/constants.js +1 -1
- package/dist/core/create-vite.js +2 -1
- package/dist/core/dev/dev.js +1 -1
- 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/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 +59 -2
- 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/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;
|
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;
|