astro 7.0.9 → 7.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/fonts/infra/fs-font-file-content-resolver.js +1 -1
- package/dist/cli/dev/index.d.ts +17 -0
- package/dist/cli/dev/index.js +56 -2
- package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
- package/dist/content/consts.d.ts +2 -0
- package/dist/content/consts.js +4 -0
- package/dist/content/content-layer.d.ts +0 -6
- package/dist/content/content-layer.js +4 -9
- package/dist/content/data-store-source.d.ts +36 -0
- package/dist/content/data-store-source.js +30 -0
- package/dist/content/data-store-writer.d.ts +67 -0
- package/dist/content/data-store-writer.js +94 -0
- package/dist/content/data-store.d.ts +15 -0
- package/dist/content/data-store.js +33 -3
- package/dist/content/loaders/glob.d.ts +7 -0
- package/dist/content/loaders/glob.js +2 -2
- package/dist/content/mutable-data-store.d.ts +8 -0
- package/dist/content/mutable-data-store.js +52 -15
- package/dist/content/paths.d.ts +14 -0
- package/dist/content/paths.js +11 -0
- package/dist/content/runtime.js +4 -4
- package/dist/content/vite-plugin-content-virtual-mod.js +29 -2
- package/dist/core/app/dev/pipeline.js +2 -1
- package/dist/core/app/types.d.ts +39 -5
- package/dist/core/base-pipeline.d.ts +1 -1
- package/dist/core/base-pipeline.js +6 -2
- package/dist/core/build/index.js +2 -1
- package/dist/core/build/plugins/plugin-manifest.js +23 -8
- package/dist/core/config/schemas/base.d.ts +38 -5
- package/dist/core/config/schemas/base.js +15 -8
- package/dist/core/config/schemas/relative.d.ts +73 -15
- package/dist/core/constants.js +1 -1
- package/dist/core/csp/common.d.ts +7 -7
- package/dist/core/csp/common.js +8 -9
- package/dist/core/csp/config.d.ts +45 -0
- package/dist/core/csp/config.js +44 -2
- package/dist/core/csp/runtime.d.ts +21 -1
- package/dist/core/csp/runtime.js +31 -0
- package/dist/core/dev/dev.js +9 -4
- package/dist/core/dev/restart.js +7 -1
- package/dist/core/errors/zod-error-map.js +7 -1
- package/dist/core/fetch/fetch-state.js +77 -29
- package/dist/core/i18n/domain.d.ts +1 -1
- package/dist/core/i18n/domain.js +4 -5
- package/dist/core/logger/config.d.ts +1 -1
- package/dist/core/logger/core.d.ts +4 -4
- package/dist/core/logger/impls/console.d.ts +2 -2
- package/dist/core/logger/impls/json.d.ts +2 -2
- package/dist/core/logger/impls/json.js +3 -14
- package/dist/core/logger/impls/node.d.ts +2 -2
- package/dist/core/logger/load.d.ts +2 -2
- package/dist/core/logger/load.js +19 -27
- package/dist/core/messages/runtime.d.ts +8 -0
- package/dist/core/messages/runtime.js +34 -1
- package/dist/core/render/paginate.js +21 -16
- package/dist/core/sync/index.js +23 -8
- package/dist/integrations/hooks.js +8 -0
- package/dist/manifest/serialized.js +21 -6
- package/dist/runtime/server/escape.d.ts +2 -0
- package/dist/runtime/server/escape.js +4 -0
- package/dist/runtime/server/render/csp.js +80 -19
- package/dist/runtime/server/render/server-islands.js +6 -4
- package/dist/runtime/server/transition.js +2 -2
- package/dist/types/astro.d.ts +2 -3
- package/dist/types/public/common.d.ts +13 -0
- package/dist/types/public/config.d.ts +196 -15
- package/dist/types/public/context.d.ts +27 -11
- package/dist/types/public/internal.d.ts +12 -11
- package/dist/vite-plugin-app/pipeline.js +2 -1
- package/dist/vite-plugin-astro-server/route-guard.js +3 -2
- package/package.json +3 -3
package/dist/cli/dev/index.d.ts
CHANGED
|
@@ -2,5 +2,22 @@ import { type Flags } from '../flags.js';
|
|
|
2
2
|
interface DevOptions {
|
|
3
3
|
flags: Flags;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* `yargs-parser` camel-cases `--ignore-lock` to `flags.ignoreLock`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isIgnoreLock(flags: Flags): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `--ignore-lock` skips the lock file entirely, so a background dev server started with it
|
|
11
|
+
* could never be found by `astro dev stop`/`status`/`logs`. Returns an error message if
|
|
12
|
+
* background mode (explicit `--background`, or implied by AI agent detection) is combined
|
|
13
|
+
* with `--ignore-lock`, or `null` if there's no conflict.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getBackgroundIgnoreLockConflict(flags: Flags, wantsBackground: boolean): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* `--force` (replace the existing server) and `--ignore-lock` (start alongside it,
|
|
18
|
+
* untracked) express contradictory intent. Returns an error message if both are set,
|
|
19
|
+
* or `null` otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getForceIgnoreLockConflict(flags: Flags): string | null;
|
|
5
22
|
export declare function dev({ flags }: DevOptions): Promise<import("../../core/dev/dev.js").DevServer | undefined>;
|
|
6
23
|
export {};
|
package/dist/cli/dev/index.js
CHANGED
|
@@ -18,6 +18,31 @@ function isRunByAgent() {
|
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
function isIgnoreLock(flags) {
|
|
22
|
+
return flags.ignoreLock === true;
|
|
23
|
+
}
|
|
24
|
+
function getBackgroundIgnoreLockConflict(flags, wantsBackground) {
|
|
25
|
+
if (!wantsBackground) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const reason = flags.background ? "`--background`" : "an auto-detected AI agent environment, which runs the dev server in the background automatically";
|
|
29
|
+
return [
|
|
30
|
+
`\`--ignore-lock\` cannot be used together with ${reason}.`,
|
|
31
|
+
"",
|
|
32
|
+
"Background dev servers rely on the lock file so `astro dev stop`, `astro dev status`, and `astro dev logs` can find them.",
|
|
33
|
+
"Run the dev server in the foreground to use --ignore-lock."
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function getForceIgnoreLockConflict(flags) {
|
|
37
|
+
if (!flags.force) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
"`--force` and `--ignore-lock` cannot be used together.",
|
|
42
|
+
"",
|
|
43
|
+
"`--force` replaces the existing dev server; `--ignore-lock` starts a new one alongside it without touching the lock file. Choose one."
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
21
46
|
async function dev({ flags }) {
|
|
22
47
|
if (flags.help || flags.h) {
|
|
23
48
|
printHelp({
|
|
@@ -37,6 +62,10 @@ async function dev({ flags }) {
|
|
|
37
62
|
["--host <custom-address>", `Expose on a network IP address at <custom-address>`],
|
|
38
63
|
["--open", "Automatically open the app in the browser on server start"],
|
|
39
64
|
["--force", "Clear the content layer cache, forcing a full rebuild."],
|
|
65
|
+
[
|
|
66
|
+
"--ignore-lock",
|
|
67
|
+
"Start the dev server even if another one is already running, without checking or writing the lock file."
|
|
68
|
+
],
|
|
40
69
|
[
|
|
41
70
|
"--allowed-hosts",
|
|
42
71
|
"Specify a comma-separated list of allowed hosts or allow any hostname."
|
|
@@ -54,6 +83,8 @@ async function dev({ flags }) {
|
|
|
54
83
|
if (agentDetected) {
|
|
55
84
|
flags.json = true;
|
|
56
85
|
}
|
|
86
|
+
const ignoreLock = isIgnoreLock(flags);
|
|
87
|
+
const wantsBackground = !!flags.background || agentDetected;
|
|
57
88
|
const logger = createLoggerFromFlags(flags);
|
|
58
89
|
const subcommand = flags._[3]?.toString();
|
|
59
90
|
if (subcommand === "stop") {
|
|
@@ -71,7 +102,13 @@ async function dev({ flags }) {
|
|
|
71
102
|
await logs({ flags, logger });
|
|
72
103
|
return;
|
|
73
104
|
}
|
|
74
|
-
if (
|
|
105
|
+
if (ignoreLock) {
|
|
106
|
+
const conflict = getBackgroundIgnoreLockConflict(flags, wantsBackground) ?? getForceIgnoreLockConflict(flags);
|
|
107
|
+
if (conflict) {
|
|
108
|
+
throw new Error(conflict);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (wantsBackground) {
|
|
75
112
|
const { background } = await import("./background.js");
|
|
76
113
|
await background({ flags, logger });
|
|
77
114
|
return;
|
|
@@ -86,6 +123,20 @@ Run \`astro dev --help\` to see available commands.`
|
|
|
86
123
|
process.exit(1);
|
|
87
124
|
}
|
|
88
125
|
const root = pathToFileURL(resolveRoot(flags.root) + "/");
|
|
126
|
+
if (ignoreLock) {
|
|
127
|
+
const existingServer2 = checkExistingServer(root);
|
|
128
|
+
if (existingServer2) {
|
|
129
|
+
logger.info(
|
|
130
|
+
"SKIP_FORMAT",
|
|
131
|
+
[
|
|
132
|
+
`Starting a new dev server alongside the one already running at ${existingServer2.url} (pid ${existingServer2.pid}).`,
|
|
133
|
+
"This instance is not tracked by `astro dev stop`, `astro dev status`, or `astro dev logs`."
|
|
134
|
+
].join("\n")
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const inlineConfig2 = flagsToAstroInlineConfig(flags);
|
|
138
|
+
return await devServer(inlineConfig2);
|
|
139
|
+
}
|
|
89
140
|
const existingServer = checkExistingServer(root);
|
|
90
141
|
if (existingServer) {
|
|
91
142
|
if (flags.force) {
|
|
@@ -121,5 +172,8 @@ Run \`astro dev --help\` to see available commands.`
|
|
|
121
172
|
return server;
|
|
122
173
|
}
|
|
123
174
|
export {
|
|
124
|
-
dev
|
|
175
|
+
dev,
|
|
176
|
+
getBackgroundIgnoreLockConflict,
|
|
177
|
+
getForceIgnoreLockConflict,
|
|
178
|
+
isIgnoreLock
|
|
125
179
|
};
|
package/dist/content/consts.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare const IMAGE_IMPORT_PREFIX = "__ASTRO_IMAGE_";
|
|
|
20
20
|
export declare const CONTENT_FLAGS: readonly ["astroContentCollectionEntry", "astroRenderContent", "astroDataCollectionEntry", "astroPropagatedAssets", "astroContentImageFlag", "astroContentModuleFlag"];
|
|
21
21
|
export declare const CONTENT_TYPES_FILE = "content.d.ts";
|
|
22
22
|
export declare const DATA_STORE_FILE = "data-store.json";
|
|
23
|
+
export declare const DATA_STORE_DIR = "data-store/";
|
|
24
|
+
export declare const DATA_STORE_MANIFEST_FILE = "manifest.json";
|
|
23
25
|
export declare const ASSET_IMPORTS_FILE = "content-assets.mjs";
|
|
24
26
|
export declare const MODULES_IMPORTS_FILE = "content-modules.mjs";
|
|
25
27
|
export declare const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
|
package/dist/content/consts.js
CHANGED
|
@@ -27,6 +27,8 @@ const CONTENT_FLAGS = [
|
|
|
27
27
|
];
|
|
28
28
|
const CONTENT_TYPES_FILE = "content.d.ts";
|
|
29
29
|
const DATA_STORE_FILE = "data-store.json";
|
|
30
|
+
const DATA_STORE_DIR = "data-store/";
|
|
31
|
+
const DATA_STORE_MANIFEST_FILE = "manifest.json";
|
|
30
32
|
const ASSET_IMPORTS_FILE = "content-assets.mjs";
|
|
31
33
|
const MODULES_IMPORTS_FILE = "content-modules.mjs";
|
|
32
34
|
const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
|
|
@@ -47,7 +49,9 @@ export {
|
|
|
47
49
|
CONTENT_RENDER_FLAG,
|
|
48
50
|
CONTENT_TYPES_FILE,
|
|
49
51
|
DATA_FLAG,
|
|
52
|
+
DATA_STORE_DIR,
|
|
50
53
|
DATA_STORE_FILE,
|
|
54
|
+
DATA_STORE_MANIFEST_FILE,
|
|
51
55
|
DATA_STORE_VIRTUAL_ID,
|
|
52
56
|
DEFERRED_MODULE,
|
|
53
57
|
IMAGE_IMPORT_PREFIX,
|
|
@@ -33,9 +33,3 @@ export declare class ContentLayer {
|
|
|
33
33
|
sync(options?: RefreshContentOptions): Promise<void>;
|
|
34
34
|
regenerateCollectionFileManifest(): Promise<void>;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Get the path to the data store file.
|
|
38
|
-
* During development, this is in the `.astro` directory so that the Vite watcher can see it.
|
|
39
|
-
* In production, it's in the cache directory so that it's preserved between builds.
|
|
40
|
-
*/
|
|
41
|
-
export declare function getDataStoreFile(settings: AstroSettings, isDev: boolean): URL;
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ASSET_IMPORTS_FILE,
|
|
8
8
|
COLLECTIONS_MANIFEST_FILE,
|
|
9
9
|
CONTENT_LAYER_TYPE,
|
|
10
|
-
DATA_STORE_FILE,
|
|
11
10
|
MODULES_IMPORTS_FILE
|
|
12
11
|
} from "./consts.js";
|
|
13
12
|
import {
|
|
@@ -197,7 +196,7 @@ ${contentConfig.error.message}`
|
|
|
197
196
|
logger.info("Content config changed");
|
|
198
197
|
shouldClear = true;
|
|
199
198
|
}
|
|
200
|
-
if (previousAstroVersion && previousAstroVersion !== "7.
|
|
199
|
+
if (previousAstroVersion && previousAstroVersion !== "7.1.1") {
|
|
201
200
|
logger.info("Astro version changed");
|
|
202
201
|
shouldClear = true;
|
|
203
202
|
}
|
|
@@ -205,8 +204,8 @@ ${contentConfig.error.message}`
|
|
|
205
204
|
logger.info("Clearing content store");
|
|
206
205
|
this.#store.clearAll();
|
|
207
206
|
}
|
|
208
|
-
if ("7.
|
|
209
|
-
this.#store.metaStore().set("astro-version", "7.
|
|
207
|
+
if ("7.1.1") {
|
|
208
|
+
this.#store.metaStore().set("astro-version", "7.1.1");
|
|
210
209
|
}
|
|
211
210
|
if (currentConfigDigest) {
|
|
212
211
|
this.#store.metaStore().set("content-config-digest", currentConfigDigest);
|
|
@@ -367,10 +366,6 @@ ${JSON.stringify({ ...raw, id: void 0 }, null, 2)}`
|
|
|
367
366
|
)
|
|
368
367
|
});
|
|
369
368
|
}
|
|
370
|
-
function getDataStoreFile(settings, isDev) {
|
|
371
|
-
return new URL(DATA_STORE_FILE, isDev ? settings.dotAstroDir : settings.config.cacheDir);
|
|
372
|
-
}
|
|
373
369
|
export {
|
|
374
|
-
ContentLayer
|
|
375
|
-
getDataStoreFile
|
|
370
|
+
ContentLayer
|
|
376
371
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DataEntry, ImmutableDataStore } from './data-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* A read-only, async view over content collection data, used at runtime by
|
|
4
|
+
* `getCollection()` and `getEntry()`. It lets the runtime read from different
|
|
5
|
+
* data sources (an in-memory snapshot today, potentially a database or remote
|
|
6
|
+
* source in the future) through one interface, without depending on Node.js
|
|
7
|
+
* APIs.
|
|
8
|
+
*
|
|
9
|
+
* The query methods are async so a source can perform I/O when resolving data.
|
|
10
|
+
* The default {@link InMemorySource} resolves synchronously.
|
|
11
|
+
*/
|
|
12
|
+
export interface DataStoreSource {
|
|
13
|
+
hasCollection(collection: string): Promise<boolean> | boolean;
|
|
14
|
+
get<T = DataEntry>(collection: string, key: string): Promise<T | undefined> | T | undefined;
|
|
15
|
+
entries<T = DataEntry>(collection: string): Promise<Array<[id: string, T]>> | Array<[id: string, T]>;
|
|
16
|
+
values<T = DataEntry>(collection: string): Promise<Array<T>> | Array<T>;
|
|
17
|
+
keys(collection: string): Promise<Array<string>> | Array<string>;
|
|
18
|
+
has(collection: string, key: string): Promise<boolean> | boolean;
|
|
19
|
+
collections(): Promise<Map<string, Map<string, any>>> | Map<string, Map<string, any>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A {@link DataStoreSource} backed by an in-memory {@link ImmutableDataStore}.
|
|
23
|
+
* All queries resolve synchronously; the async signatures exist to satisfy the
|
|
24
|
+
* {@link DataStoreSource} contract.
|
|
25
|
+
*/
|
|
26
|
+
export declare class InMemorySource implements DataStoreSource {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(store: ImmutableDataStore);
|
|
29
|
+
hasCollection(collection: string): boolean;
|
|
30
|
+
get<T = DataEntry>(collection: string, key: string): T | undefined;
|
|
31
|
+
entries<T = DataEntry>(collection: string): Array<[id: string, T]>;
|
|
32
|
+
values<T = DataEntry>(collection: string): Array<T>;
|
|
33
|
+
keys(collection: string): Array<string>;
|
|
34
|
+
has(collection: string, key: string): boolean;
|
|
35
|
+
collections(): Map<string, Map<string, any>>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class InMemorySource {
|
|
2
|
+
#store;
|
|
3
|
+
constructor(store) {
|
|
4
|
+
this.#store = store;
|
|
5
|
+
}
|
|
6
|
+
hasCollection(collection) {
|
|
7
|
+
return this.#store.hasCollection(collection);
|
|
8
|
+
}
|
|
9
|
+
get(collection, key) {
|
|
10
|
+
return this.#store.get(collection, key);
|
|
11
|
+
}
|
|
12
|
+
entries(collection) {
|
|
13
|
+
return this.#store.entries(collection);
|
|
14
|
+
}
|
|
15
|
+
values(collection) {
|
|
16
|
+
return this.#store.values(collection);
|
|
17
|
+
}
|
|
18
|
+
keys(collection) {
|
|
19
|
+
return this.#store.keys(collection);
|
|
20
|
+
}
|
|
21
|
+
has(collection, key) {
|
|
22
|
+
return this.#store.has(collection, key);
|
|
23
|
+
}
|
|
24
|
+
collections() {
|
|
25
|
+
return this.#store.collections();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
InMemorySource
|
|
30
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type PathLike } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* A chunked store manifest: each collection maps to the list of part file names
|
|
4
|
+
* whose contents concatenate back into that collection's serialized string.
|
|
5
|
+
*/
|
|
6
|
+
export type DataStoreManifest = Record<string, string[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Persists the content collection data produced by the content layer. This is
|
|
9
|
+
* the write side that saves the data; {@link import('./data-store-source.js').DataStoreSource}
|
|
10
|
+
* is the read side that loads it back. Implementations run in Node.js
|
|
11
|
+
* (build/dev) and are never imported at runtime.
|
|
12
|
+
*/
|
|
13
|
+
export interface DataStoreWriter {
|
|
14
|
+
/** Serialize and persist the given collections. */
|
|
15
|
+
write(collections: Map<string, Map<string, any>>): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Serialize collections to a deterministic devalue string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function serializeDataStore(collections: Map<string, Map<string, any>>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Split a string into parts each at most `maxBytes` UTF-8 bytes, never splitting
|
|
23
|
+
* a Unicode code point across parts.
|
|
24
|
+
*
|
|
25
|
+
* The store is serialized with devalue and written to disk as UTF-8. Splitting
|
|
26
|
+
* on UTF-16 code-unit boundaries (e.g. `String.prototype.slice`) can cut a
|
|
27
|
+
* surrogate pair in half; encoding a lone surrogate to UTF-8 substitutes U+FFFD,
|
|
28
|
+
* so concatenating the re-read parts would corrupt any astral-plane character
|
|
29
|
+
* (emoji, some CJK, etc.). Iterating with `for...of` yields whole code points,
|
|
30
|
+
* so `str.slice` is only ever called on code-point boundaries and the parts
|
|
31
|
+
* always rejoin to the exact original string.
|
|
32
|
+
*/
|
|
33
|
+
export declare function chunkString(str: string, maxBytes: number): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Atomically write `data` to `file`.
|
|
36
|
+
*
|
|
37
|
+
* The data is written to a temporary file and then renamed into place to avoid
|
|
38
|
+
* partial reads. If the file already contains identical data, the write is
|
|
39
|
+
* skipped. Callers are responsible for serializing concurrent writes to the
|
|
40
|
+
* same file.
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeFileAtomic(file: PathLike, data: string): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* A {@link DataStoreWriter} that serializes the whole store to a single file.
|
|
45
|
+
*/
|
|
46
|
+
export declare class FileWriter implements DataStoreWriter {
|
|
47
|
+
#private;
|
|
48
|
+
constructor(file: PathLike);
|
|
49
|
+
write(collections: Map<string, Map<string, any>>): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A {@link DataStoreWriter} that splits the store across many content-addressed
|
|
53
|
+
* files inside a directory, described by a manifest.
|
|
54
|
+
*
|
|
55
|
+
* Each collection is serialized to a string and split into parts no larger than
|
|
56
|
+
* a fixed byte size, so no single file grows unbounded (platform file-size
|
|
57
|
+
* limits). Each part file is named by the xxhash of its contents, so unchanged
|
|
58
|
+
* parts keep the same name across builds and their writes are skipped, and two
|
|
59
|
+
* identical parts are naturally deduplicated. The manifest is written last as
|
|
60
|
+
* the commit point, and stale files are pruned afterwards. This is the inverse
|
|
61
|
+
* of {@link import('./data-store.js').ImmutableDataStore.manifestToMap}.
|
|
62
|
+
*/
|
|
63
|
+
export declare class ChunkedWriter implements DataStoreWriter {
|
|
64
|
+
#private;
|
|
65
|
+
constructor(dir: URL);
|
|
66
|
+
write(collections: Map<string, Map<string, any>>): Promise<void>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as devalue from "devalue";
|
|
3
|
+
import xxhash, {} from "xxhash-wasm";
|
|
4
|
+
import { emptyDir } from "../core/fs/index.js";
|
|
5
|
+
import { DATA_STORE_MANIFEST_FILE } from "./consts.js";
|
|
6
|
+
const CHUNK_SIZE_LIMIT = 20 * 1024 * 1024;
|
|
7
|
+
function sortCollections(collections) {
|
|
8
|
+
return new Map(
|
|
9
|
+
[...collections.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, collection]) => [
|
|
10
|
+
key,
|
|
11
|
+
new Map([...collection.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
12
|
+
])
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function serializeDataStore(collections) {
|
|
16
|
+
return devalue.stringify(sortCollections(collections));
|
|
17
|
+
}
|
|
18
|
+
const ENCODER = new TextEncoder();
|
|
19
|
+
function chunkString(str, maxBytes) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let startIndex = 0;
|
|
22
|
+
let index = 0;
|
|
23
|
+
let currentBytes = 0;
|
|
24
|
+
for (const char of str) {
|
|
25
|
+
const charBytes = ENCODER.encode(char).length;
|
|
26
|
+
if (currentBytes + charBytes > maxBytes && index > startIndex) {
|
|
27
|
+
chunks.push(str.slice(startIndex, index));
|
|
28
|
+
startIndex = index;
|
|
29
|
+
currentBytes = 0;
|
|
30
|
+
}
|
|
31
|
+
index += char.length;
|
|
32
|
+
currentBytes += charBytes;
|
|
33
|
+
}
|
|
34
|
+
if (startIndex < str.length) {
|
|
35
|
+
chunks.push(str.slice(startIndex));
|
|
36
|
+
}
|
|
37
|
+
return chunks;
|
|
38
|
+
}
|
|
39
|
+
async function writeFileAtomic(file, data) {
|
|
40
|
+
const tempFile = file instanceof URL ? new URL(`${file.href}.tmp`) : `${file}.tmp`;
|
|
41
|
+
const oldData = await fs.readFile(file, "utf-8").catch(() => "");
|
|
42
|
+
if (oldData === data) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await fs.writeFile(tempFile, data);
|
|
46
|
+
await fs.rename(tempFile, file);
|
|
47
|
+
}
|
|
48
|
+
class FileWriter {
|
|
49
|
+
#file;
|
|
50
|
+
constructor(file) {
|
|
51
|
+
this.#file = file;
|
|
52
|
+
}
|
|
53
|
+
async write(collections) {
|
|
54
|
+
await writeFileAtomic(this.#file, serializeDataStore(collections));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
class ChunkedWriter {
|
|
58
|
+
#dir;
|
|
59
|
+
#manifestFile;
|
|
60
|
+
#hasher;
|
|
61
|
+
constructor(dir) {
|
|
62
|
+
this.#dir = dir;
|
|
63
|
+
this.#manifestFile = new URL(`./${DATA_STORE_MANIFEST_FILE}`, dir);
|
|
64
|
+
}
|
|
65
|
+
async write(collections) {
|
|
66
|
+
if (!this.#hasher) {
|
|
67
|
+
this.#hasher = await xxhash();
|
|
68
|
+
}
|
|
69
|
+
const { h64ToString } = this.#hasher;
|
|
70
|
+
const writtenFiles = /* @__PURE__ */ new Set();
|
|
71
|
+
const manifest = {};
|
|
72
|
+
for (const [collectionName, entries] of sortCollections(collections)) {
|
|
73
|
+
const stringified = devalue.stringify(entries);
|
|
74
|
+
const parts = [];
|
|
75
|
+
for (const part of chunkString(stringified, CHUNK_SIZE_LIMIT)) {
|
|
76
|
+
const fileName = `${h64ToString(part)}.txt`;
|
|
77
|
+
await writeFileAtomic(new URL(`./${fileName}`, this.#dir), part);
|
|
78
|
+
parts.push(fileName);
|
|
79
|
+
writtenFiles.add(fileName);
|
|
80
|
+
}
|
|
81
|
+
manifest[collectionName] = parts;
|
|
82
|
+
}
|
|
83
|
+
await writeFileAtomic(this.#manifestFile, JSON.stringify(manifest));
|
|
84
|
+
writtenFiles.add(DATA_STORE_MANIFEST_FILE);
|
|
85
|
+
emptyDir(this.#dir, writtenFiles);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
ChunkedWriter,
|
|
90
|
+
FileWriter,
|
|
91
|
+
chunkString,
|
|
92
|
+
serializeDataStore,
|
|
93
|
+
writeFileAtomic
|
|
94
|
+
};
|
|
@@ -46,6 +46,21 @@ export declare class ImmutableDataStore {
|
|
|
46
46
|
has(collectionName: string, key: string): boolean;
|
|
47
47
|
hasCollection(collectionName: string): boolean;
|
|
48
48
|
collections(): Map<string, Map<string, any>>;
|
|
49
|
+
/**
|
|
50
|
+
* Rebuilds a collections map from a chunked-store manifest whose part file
|
|
51
|
+
* names have already been swapped for their contents.
|
|
52
|
+
*
|
|
53
|
+
* Each collection maps to a list of parts. A part is either a raw string
|
|
54
|
+
* (when the store is loaded from disk) or an ESM namespace from a `?raw`
|
|
55
|
+
* import (`{ default: string }`, when emitted into the virtual module at
|
|
56
|
+
* runtime). A collection's parts are concatenated back into the exact
|
|
57
|
+
* serialized string, then parsed with devalue. This is the inverse of
|
|
58
|
+
* {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
|
|
59
|
+
* Node built-ins so it can run at runtime.
|
|
60
|
+
*/
|
|
61
|
+
static manifestToMap(manifest: Record<string, Array<string | {
|
|
62
|
+
default: string;
|
|
63
|
+
}>>): Map<string, Map<string, any>>;
|
|
49
64
|
/**
|
|
50
65
|
* Attempts to load a DataStore from the virtual module.
|
|
51
66
|
* This only works in Vite.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as devalue from "devalue";
|
|
2
|
+
import { InMemorySource } from "./data-store-source.js";
|
|
2
3
|
class ImmutableDataStore {
|
|
3
4
|
_collections = /* @__PURE__ */ new Map();
|
|
4
5
|
constructor() {
|
|
@@ -32,6 +33,30 @@ class ImmutableDataStore {
|
|
|
32
33
|
collections() {
|
|
33
34
|
return this._collections;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Rebuilds a collections map from a chunked-store manifest whose part file
|
|
38
|
+
* names have already been swapped for their contents.
|
|
39
|
+
*
|
|
40
|
+
* Each collection maps to a list of parts. A part is either a raw string
|
|
41
|
+
* (when the store is loaded from disk) or an ESM namespace from a `?raw`
|
|
42
|
+
* import (`{ default: string }`, when emitted into the virtual module at
|
|
43
|
+
* runtime). A collection's parts are concatenated back into the exact
|
|
44
|
+
* serialized string, then parsed with devalue. This is the inverse of
|
|
45
|
+
* {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
|
|
46
|
+
* Node built-ins so it can run at runtime.
|
|
47
|
+
*/
|
|
48
|
+
static manifestToMap(manifest) {
|
|
49
|
+
const collections = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const [collectionName, parts] of Object.entries(manifest)) {
|
|
51
|
+
let stringified = "";
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
stringified += typeof part === "string" ? part : part.default;
|
|
54
|
+
}
|
|
55
|
+
const entries = devalue.parse(stringified);
|
|
56
|
+
collections.set(collectionName, entries);
|
|
57
|
+
}
|
|
58
|
+
return collections;
|
|
59
|
+
}
|
|
35
60
|
/**
|
|
36
61
|
* Attempts to load a DataStore from the virtual module.
|
|
37
62
|
* This only works in Vite.
|
|
@@ -42,7 +67,11 @@ class ImmutableDataStore {
|
|
|
42
67
|
if (data.default instanceof Map) {
|
|
43
68
|
return ImmutableDataStore.fromMap(data.default);
|
|
44
69
|
}
|
|
45
|
-
|
|
70
|
+
if (Array.isArray(data.default)) {
|
|
71
|
+
const map2 = devalue.unflatten(data.default);
|
|
72
|
+
return ImmutableDataStore.fromMap(map2);
|
|
73
|
+
}
|
|
74
|
+
const map = ImmutableDataStore.manifestToMap(data.default);
|
|
46
75
|
return ImmutableDataStore.fromMap(map);
|
|
47
76
|
} catch {
|
|
48
77
|
}
|
|
@@ -59,12 +88,13 @@ function dataStoreSingleton() {
|
|
|
59
88
|
return {
|
|
60
89
|
get: async () => {
|
|
61
90
|
if (!instance) {
|
|
62
|
-
instance = ImmutableDataStore.fromModule();
|
|
91
|
+
instance = ImmutableDataStore.fromModule().then((store) => new InMemorySource(store));
|
|
63
92
|
}
|
|
64
93
|
return instance;
|
|
65
94
|
},
|
|
95
|
+
// Note: currently unused, but kept for API stability.
|
|
66
96
|
set: (store) => {
|
|
67
|
-
instance = store;
|
|
97
|
+
instance = new InMemorySource(store);
|
|
68
98
|
}
|
|
69
99
|
};
|
|
70
100
|
}
|
|
@@ -23,6 +23,13 @@ interface GlobOptions {
|
|
|
23
23
|
* Defaults to `true`.
|
|
24
24
|
*/
|
|
25
25
|
retainBody?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When `true`, renderable content entries (e.g. Markdown) will not be eagerly rendered
|
|
28
|
+
* during content sync. Instead, rendering is deferred until the entry is actually rendered
|
|
29
|
+
* in a page. This reduces memory usage for large collections with heavy rendered output.
|
|
30
|
+
* Defaults to `false`.
|
|
31
|
+
*/
|
|
32
|
+
deferRender?: boolean;
|
|
26
33
|
}
|
|
27
34
|
export declare const secretLegacyFlag: unique symbol;
|
|
28
35
|
/**
|
|
@@ -111,7 +111,7 @@ function glob(globOptions) {
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
-
if (entryType.getRenderFunction) {
|
|
114
|
+
if (entryType.getRenderFunction && !globOptions.deferRender) {
|
|
115
115
|
let render = renderFunctionByContentType.get(entryType);
|
|
116
116
|
if (!render) {
|
|
117
117
|
render = await entryType.getRenderFunction(config);
|
|
@@ -138,7 +138,7 @@ function glob(globOptions) {
|
|
|
138
138
|
rendered,
|
|
139
139
|
assetImports: rendered?.metadata?.imagePaths
|
|
140
140
|
});
|
|
141
|
-
} else if ("contentModuleTypes" in entryType) {
|
|
141
|
+
} else if (entryType.getRenderFunction && globOptions.deferRender || "contentModuleTypes" in entryType) {
|
|
142
142
|
store.set({
|
|
143
143
|
id,
|
|
144
144
|
data: parsedData,
|
|
@@ -35,6 +35,14 @@ export declare class MutableDataStore extends ImmutableDataStore {
|
|
|
35
35
|
static fromMap(data: Map<string, Map<string, any>>): Promise<MutableDataStore>;
|
|
36
36
|
static fromString(data: string): Promise<MutableDataStore>;
|
|
37
37
|
static fromFile(filePath: string | URL): Promise<MutableDataStore>;
|
|
38
|
+
/**
|
|
39
|
+
* Loads a MutableDataStore from a chunked store directory (experimental
|
|
40
|
+
* `collectionStorage: 'chunked'`), reading the manifest and its referenced parts.
|
|
41
|
+
* If the directory has no manifest yet (fresh build) it starts empty. If the
|
|
42
|
+
* manifest exists but can't be read (corrupt cache), it warns and starts
|
|
43
|
+
* empty so loaders rebuild it, rather than failing the sync.
|
|
44
|
+
*/
|
|
45
|
+
static fromDir(dirPath: URL): Promise<MutableDataStore>;
|
|
38
46
|
}
|
|
39
47
|
export interface DataStore {
|
|
40
48
|
get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) => DataEntry<TData> | undefined;
|