@stati/core 1.27.2 → 1.29.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/README.md +6 -1
- package/dist/config/errors.d.ts +10 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +10 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -2
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +23 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +201 -41
- package/dist/core/collections.d.ts +139 -0
- package/dist/core/collections.d.ts.map +1 -0
- package/dist/core/collections.js +342 -0
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +2 -1
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +11 -62
- package/dist/core/expand.d.ts +86 -0
- package/dist/core/expand.d.ts.map +1 -0
- package/dist/core/expand.js +484 -0
- package/dist/core/isg/builder.d.ts +27 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +83 -11
- package/dist/core/isg/deps.d.ts +9 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +30 -1
- package/dist/core/isg/hash.d.ts +14 -2
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +30 -2
- package/dist/core/isg/index.d.ts +4 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +4 -2
- package/dist/core/isg/manifest.js +14 -0
- package/dist/core/isg/prune.d.ts +32 -0
- package/dist/core/isg/prune.d.ts.map +1 -0
- package/dist/core/isg/prune.js +113 -0
- package/dist/core/markdown.d.ts +10 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +13 -3
- package/dist/core/navigation.d.ts +4 -2
- package/dist/core/navigation.d.ts.map +1 -1
- package/dist/core/navigation.js +17 -2
- package/dist/core/templates.d.ts +15 -1
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +121 -126
- package/dist/core/utils/fs.utils.d.ts +29 -1
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +74 -1
- package/dist/core/utils/index.d.ts +3 -3
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -3
- package/dist/core/utils/paths.utils.d.ts +15 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +20 -0
- package/dist/core/utils/template-discovery.utils.d.ts +25 -1
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +61 -11
- package/dist/images/config.d.ts +52 -0
- package/dist/images/config.d.ts.map +1 -0
- package/dist/images/config.js +200 -0
- package/dist/images/index.d.ts +15 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +11 -0
- package/dist/images/manifest.d.ts +36 -0
- package/dist/images/manifest.d.ts.map +1 -0
- package/dist/images/manifest.js +69 -0
- package/dist/images/markdown-rule.d.ts +26 -0
- package/dist/images/markdown-rule.d.ts.map +1 -0
- package/dist/images/markdown-rule.js +60 -0
- package/dist/images/markup.d.ts +39 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +72 -0
- package/dist/images/og.d.ts +52 -0
- package/dist/images/og.d.ts.map +1 -0
- package/dist/images/og.js +165 -0
- package/dist/images/plan.d.ts +101 -0
- package/dist/images/plan.d.ts.map +1 -0
- package/dist/images/plan.js +128 -0
- package/dist/images/provider.d.ts +35 -0
- package/dist/images/provider.d.ts.map +1 -0
- package/dist/images/provider.js +70 -0
- package/dist/images/resolve.d.ts +45 -0
- package/dist/images/resolve.d.ts.map +1 -0
- package/dist/images/resolve.js +51 -0
- package/dist/images/runtime.d.ts +258 -0
- package/dist/images/runtime.d.ts.map +1 -0
- package/dist/images/runtime.js +659 -0
- package/dist/images/sources.d.ts +47 -0
- package/dist/images/sources.d.ts.map +1 -0
- package/dist/images/sources.js +97 -0
- package/dist/images/utils/image-probe.utils.d.ts +30 -0
- package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
- package/dist/images/utils/image-probe.utils.js +307 -0
- package/dist/images/utils/index.d.ts +7 -0
- package/dist/images/utils/index.d.ts.map +1 -0
- package/dist/images/utils/index.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +3 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +3 -0
- package/dist/metrics/types.d.ts +8 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/rss/generator.d.ts.map +1 -1
- package/dist/rss/generator.js +5 -2
- package/dist/search/generator.d.ts +9 -0
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +20 -0
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -1
- package/dist/seo/auto-inject.d.ts +3 -0
- package/dist/seo/auto-inject.d.ts.map +1 -1
- package/dist/seo/auto-inject.js +1 -0
- package/dist/seo/generator.d.ts +24 -0
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +65 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/types/config.d.ts +60 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +150 -3
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/images.d.ts +185 -0
- package/dist/types/images.d.ts.map +1 -0
- package/dist/types/images.js +8 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +19 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/dist/types/seo.d.ts +9 -0
- package/dist/types/seo.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { PageModel, CollectionData, PaginationData, GeneratedPageData, StatiConfig } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Front matter fields of the collection summary when `collections.summaryFields` is not set.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_SUMMARY_FIELDS: readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* The collections of a page list.
|
|
8
|
+
*/
|
|
9
|
+
export interface CollectionIndex {
|
|
10
|
+
/** Collection data by the URL of its index page */
|
|
11
|
+
byPath: Map<string, CollectionData>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What a render read from data shared between pages. Filled in while the page renders.
|
|
15
|
+
*/
|
|
16
|
+
export interface PageReads {
|
|
17
|
+
/** The page read `stati.collection` */
|
|
18
|
+
collection: boolean;
|
|
19
|
+
/** The page read the navigation tree (`stati.nav` or `stati.page.navNode`) */
|
|
20
|
+
nav: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the collections of a page list, computing them once per array.
|
|
24
|
+
*
|
|
25
|
+
* @param allPages - All pages of the build
|
|
26
|
+
* @returns The collections by index page URL
|
|
27
|
+
*/
|
|
28
|
+
export declare function getCollections(allPages: PageModel[]): CollectionIndex;
|
|
29
|
+
/**
|
|
30
|
+
* Computes every collection of a page list in one pass.
|
|
31
|
+
*
|
|
32
|
+
* A collection belongs to a collection index page (see `isCollectionIndexPage`), or to a page
|
|
33
|
+
* loaded from an `index.md`, whose collection is empty while its folder holds no pages. The
|
|
34
|
+
* root collection holds the top-level pages; any other collection holds every page below its
|
|
35
|
+
* folder, at any depth. The index page itself, generated pages and pagination pages are
|
|
36
|
+
* never members.
|
|
37
|
+
*
|
|
38
|
+
* @param allPages - All pages of the build
|
|
39
|
+
* @returns The collections by index page URL
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildCollections(allPages: PageModel[]): CollectionIndex;
|
|
42
|
+
/**
|
|
43
|
+
* Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
|
|
44
|
+
*
|
|
45
|
+
* @param pages - Pages to sort, in place
|
|
46
|
+
* @returns The sorted array
|
|
47
|
+
*/
|
|
48
|
+
export declare function sortPagesByDate(pages: PageModel[]): PageModel[];
|
|
49
|
+
/**
|
|
50
|
+
* Finds the collection a `collection` front matter value names. Accepts the index page URL
|
|
51
|
+
* with or without a leading or trailing slash: `posts`, `/posts` and `/posts/` all name the
|
|
52
|
+
* collection of `site/posts/index.md`.
|
|
53
|
+
*
|
|
54
|
+
* @param index - The collections of the build
|
|
55
|
+
* @param value - The front matter value
|
|
56
|
+
* @returns The index page URL, or undefined when the value names no collection
|
|
57
|
+
*/
|
|
58
|
+
export declare function findCollectionPath(index: CollectionIndex, value: unknown): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Finds the collection a page reads as `stati.collection`:
|
|
61
|
+
*
|
|
62
|
+
* 1. The collection its `collection` front matter field names, when that collection exists.
|
|
63
|
+
* 2. Its own collection, when the page is a collection index page or an `index.md`, even
|
|
64
|
+
* when that collection is empty.
|
|
65
|
+
* 3. Otherwise the nearest collection with pages that the page belongs to: `/blog/post` reads
|
|
66
|
+
* `/blog`, and a top-level page reads the root collection.
|
|
67
|
+
*
|
|
68
|
+
* Pagination pages after the first read the collection of the first page. Generated pages
|
|
69
|
+
* read only the collection their template names; expansion sets it in their front matter.
|
|
70
|
+
*
|
|
71
|
+
* @param page - The page
|
|
72
|
+
* @param allPages - All pages of the build
|
|
73
|
+
* @returns The index page URL of the collection, or undefined when the page has none
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveCollectionPath(page: PageModel, allPages: PageModel[]): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Returns the front matter fields of the collection summary.
|
|
78
|
+
*
|
|
79
|
+
* @param config - Stati configuration
|
|
80
|
+
* @returns `collections.summaryFields`, or the default fields
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveSummaryFields(config: StatiConfig): readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Hashes what a listing shows of a list of pages: each page's URL, publish date and summary
|
|
85
|
+
* fields, in list order. The Markdown body only counts when `fields` includes `content`.
|
|
86
|
+
*
|
|
87
|
+
* @param pages - The listed pages
|
|
88
|
+
* @param fields - Front matter fields to include
|
|
89
|
+
* @returns A `sha256-…` hash
|
|
90
|
+
*/
|
|
91
|
+
export declare function computePagesSummaryHash(pages: readonly PageModel[], fields: readonly string[]): string;
|
|
92
|
+
/**
|
|
93
|
+
* Collection summary hashes for one build, computed at most once per collection.
|
|
94
|
+
*/
|
|
95
|
+
export interface SummaryHasher {
|
|
96
|
+
/** Summary hash of a collection, or `'none'` when there is no such collection */
|
|
97
|
+
collection(path: string): string;
|
|
98
|
+
/** Summary hash of a list of pages */
|
|
99
|
+
pages(pages: readonly PageModel[]): string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Creates the summary hasher of a build. Call it after the `beforeAll` hook, which can change
|
|
103
|
+
* the front matter the summaries include.
|
|
104
|
+
*
|
|
105
|
+
* @param allPages - All pages of the build
|
|
106
|
+
* @param fields - Summary fields
|
|
107
|
+
* @returns The hasher
|
|
108
|
+
*/
|
|
109
|
+
export declare function createSummaryHasher(allPages: PageModel[], fields: readonly string[]): SummaryHasher;
|
|
110
|
+
/**
|
|
111
|
+
* Wraps collection data for one render. Reading any of its properties sets `reads.collection`,
|
|
112
|
+
* so the cache knows the page depends on the collection. Arrays and objects are copied on
|
|
113
|
+
* first read, so a template that sorts or changes them cannot affect other pages, which share
|
|
114
|
+
* the collection data within a build.
|
|
115
|
+
*
|
|
116
|
+
* The properties are accessors on a plain object, not a Proxy, so spreading the template
|
|
117
|
+
* context (as callable partials do) passes the view along without reading it.
|
|
118
|
+
*
|
|
119
|
+
* @param data - The shared collection data
|
|
120
|
+
* @param reads - The render's read flags
|
|
121
|
+
* @returns The view for `stati.collection`
|
|
122
|
+
*/
|
|
123
|
+
export declare function createCollectionView(data: CollectionData, reads: PageReads): CollectionData;
|
|
124
|
+
/**
|
|
125
|
+
* Copies a page's pagination data for `stati.pagination`, so a template that changes the
|
|
126
|
+
* lists cannot change what the cache hashes.
|
|
127
|
+
*
|
|
128
|
+
* @param data - The page's pagination data
|
|
129
|
+
* @returns A copy with its own arrays
|
|
130
|
+
*/
|
|
131
|
+
export declare function copyPaginationData(data: PaginationData): PaginationData;
|
|
132
|
+
/**
|
|
133
|
+
* Copies a page's generated data for `stati.generated`.
|
|
134
|
+
*
|
|
135
|
+
* @param data - The page's generated data
|
|
136
|
+
* @returns A copy with its own items array
|
|
137
|
+
*/
|
|
138
|
+
export declare function copyGeneratedData(data: GeneratedPageData): GeneratedPageData;
|
|
139
|
+
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/core/collections.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAU3B;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAA8C,CAAC;AAEnG;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,GAAG,EAAE,OAAO,CAAC;CACd;AASD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAQrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAwBvE;AAwFD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAc/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAc7F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAuBhG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAM3E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,MAAM,CAWR;AAMD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,sCAAsC;IACtC,KAAK,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAAC;CAC5C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,SAAS,EAAE,EACrB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,aAAa,CAcf;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,GAAG,cAAc,CAwB3F;AAaD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,CAE5E"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { getCollectionPathForPage, getFirstPaginationPage, isCollectionIndexPage, isVirtualPage, } from './utils/index.js';
|
|
3
|
+
import { computeContentHash } from './isg/hash.js';
|
|
4
|
+
import { MARKDOWN_EXTENSION } from '../constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Front matter fields of the collection summary when `collections.summaryFields` is not set.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_SUMMARY_FIELDS = ['title', 'description', 'tags', 'draft'];
|
|
9
|
+
/**
|
|
10
|
+
* Collections per page list, computed at the first lookup for an array and reused after that.
|
|
11
|
+
* Like the directory prefix index in `isCollectionIndexPage`, the result is a snapshot: in a
|
|
12
|
+
* build the first lookup happens in the render loop, after the `beforeAll` hook.
|
|
13
|
+
*/
|
|
14
|
+
const collectionCache = new WeakMap();
|
|
15
|
+
/**
|
|
16
|
+
* Returns the collections of a page list, computing them once per array.
|
|
17
|
+
*
|
|
18
|
+
* @param allPages - All pages of the build
|
|
19
|
+
* @returns The collections by index page URL
|
|
20
|
+
*/
|
|
21
|
+
export function getCollections(allPages) {
|
|
22
|
+
const cached = collectionCache.get(allPages);
|
|
23
|
+
if (cached) {
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
const index = buildCollections(allPages);
|
|
27
|
+
collectionCache.set(allPages, index);
|
|
28
|
+
return index;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Computes every collection of a page list in one pass.
|
|
32
|
+
*
|
|
33
|
+
* A collection belongs to a collection index page (see `isCollectionIndexPage`), or to a page
|
|
34
|
+
* loaded from an `index.md`, whose collection is empty while its folder holds no pages. The
|
|
35
|
+
* root collection holds the top-level pages; any other collection holds every page below its
|
|
36
|
+
* folder, at any depth. The index page itself, generated pages and pagination pages are
|
|
37
|
+
* never members.
|
|
38
|
+
*
|
|
39
|
+
* @param allPages - All pages of the build
|
|
40
|
+
* @returns The collections by index page URL
|
|
41
|
+
*/
|
|
42
|
+
export function buildCollections(allPages) {
|
|
43
|
+
const pages = allPages.filter((page) => !isVirtualPage(page));
|
|
44
|
+
const members = new Map();
|
|
45
|
+
for (const page of pages) {
|
|
46
|
+
if (!members.has(page.url) &&
|
|
47
|
+
(isFolderIndexPage(page) || isCollectionIndexPage(page, allPages))) {
|
|
48
|
+
members.set(page.url, []);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const page of pages) {
|
|
52
|
+
for (const path of collectionsContaining(page.url, (path) => members.has(path))) {
|
|
53
|
+
members.get(path).push(page);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const byPath = new Map();
|
|
57
|
+
for (const [path, collectionPages] of members) {
|
|
58
|
+
byPath.set(path, createCollectionData(path, collectionPages));
|
|
59
|
+
}
|
|
60
|
+
return { byPath };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a page was loaded from a folder's `index.md`. The folder is a collection even before
|
|
64
|
+
* it holds any pages, so an empty blog lists no posts instead of reading another collection.
|
|
65
|
+
*/
|
|
66
|
+
function isFolderIndexPage(page) {
|
|
67
|
+
return basename(page.sourcePath, MARKDOWN_EXTENSION) === 'index';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Lists the collections a URL belongs to, outermost first.
|
|
71
|
+
*
|
|
72
|
+
* @param url - A page URL
|
|
73
|
+
* @param isCollection - Whether a URL is a collection index page
|
|
74
|
+
* @returns Index page URLs
|
|
75
|
+
*/
|
|
76
|
+
function collectionsContaining(url, isCollection) {
|
|
77
|
+
const found = [];
|
|
78
|
+
// Every folder the URL lives in, from the outermost: /blog/2024/recap is in /blog/ and /blog/2024/
|
|
79
|
+
for (let i = url.indexOf('/', 1); i !== -1; i = url.indexOf('/', i + 1)) {
|
|
80
|
+
const folder = url.slice(0, i + 1);
|
|
81
|
+
// A folder's index page is usually /blog, or /blog/ with a trailing-slash permalink
|
|
82
|
+
for (const path of [folder.slice(0, -1), folder]) {
|
|
83
|
+
if (path !== url && isCollection(path)) {
|
|
84
|
+
found.push(path);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// The root collection is shallow: only pages whose parent is the root belong to it
|
|
89
|
+
if (url !== '/' && isCollection('/') && getCollectionPathForPage(url) === '/') {
|
|
90
|
+
found.push('/');
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Builds the collection data of one index page from its members.
|
|
96
|
+
*/
|
|
97
|
+
function createCollectionData(collectionPath, collectionPages) {
|
|
98
|
+
// Direct children are one level below the index page
|
|
99
|
+
const children = collectionPages.filter((page) => {
|
|
100
|
+
const relativePath = page.url.substring(collectionPath.length);
|
|
101
|
+
const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
|
102
|
+
return !cleanPath.includes('/');
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
pages: collectionPages,
|
|
106
|
+
children,
|
|
107
|
+
recentPages: sortPagesByDate([...collectionPages]),
|
|
108
|
+
pagesByTag: groupPagesByTags(collectionPages),
|
|
109
|
+
metadata: {
|
|
110
|
+
totalPages: collectionPages.length,
|
|
111
|
+
hasChildren: children.length > 0,
|
|
112
|
+
collectionPath,
|
|
113
|
+
collectionName: collectionPath === '/' ? 'Home' : basename(collectionPath),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Groups pages by their tags.
|
|
119
|
+
*
|
|
120
|
+
* @param pages - Pages to group
|
|
121
|
+
* @returns Object mapping tag names to arrays of pages
|
|
122
|
+
*/
|
|
123
|
+
function groupPagesByTags(pages) {
|
|
124
|
+
// No prototype, so tags such as `constructor` or `__proto__` are keys like any other
|
|
125
|
+
const pagesByTag = Object.create(null);
|
|
126
|
+
for (const page of pages) {
|
|
127
|
+
const tags = page.frontMatter.tags;
|
|
128
|
+
if (Array.isArray(tags)) {
|
|
129
|
+
for (const tag of tags) {
|
|
130
|
+
if (typeof tag === 'string') {
|
|
131
|
+
(pagesByTag[tag] ??= []).push(page);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return pagesByTag;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
|
|
140
|
+
*
|
|
141
|
+
* @param pages - Pages to sort, in place
|
|
142
|
+
* @returns The sorted array
|
|
143
|
+
*/
|
|
144
|
+
export function sortPagesByDate(pages) {
|
|
145
|
+
return pages.sort((a, b) => {
|
|
146
|
+
// Sort by publishedAt (newest first)
|
|
147
|
+
if (a.publishedAt && b.publishedAt) {
|
|
148
|
+
return b.publishedAt.getTime() - a.publishedAt.getTime();
|
|
149
|
+
}
|
|
150
|
+
if (a.publishedAt && !b.publishedAt)
|
|
151
|
+
return -1;
|
|
152
|
+
if (!a.publishedAt && b.publishedAt)
|
|
153
|
+
return 1;
|
|
154
|
+
// Fallback to title
|
|
155
|
+
const titleA = a.frontMatter.title || basename(a.url) || 'Untitled';
|
|
156
|
+
const titleB = b.frontMatter.title || basename(b.url) || 'Untitled';
|
|
157
|
+
return titleA.localeCompare(titleB);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Finds the collection a `collection` front matter value names. Accepts the index page URL
|
|
162
|
+
* with or without a leading or trailing slash: `posts`, `/posts` and `/posts/` all name the
|
|
163
|
+
* collection of `site/posts/index.md`.
|
|
164
|
+
*
|
|
165
|
+
* @param index - The collections of the build
|
|
166
|
+
* @param value - The front matter value
|
|
167
|
+
* @returns The index page URL, or undefined when the value names no collection
|
|
168
|
+
*/
|
|
169
|
+
export function findCollectionPath(index, value) {
|
|
170
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const trimmed = value.trim();
|
|
174
|
+
const url = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
175
|
+
if (index.byPath.has(url)) {
|
|
176
|
+
return url;
|
|
177
|
+
}
|
|
178
|
+
if (url === '/') {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const alternate = url.endsWith('/') ? url.slice(0, -1) : `${url}/`;
|
|
182
|
+
return index.byPath.has(alternate) ? alternate : undefined;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Finds the collection a page reads as `stati.collection`:
|
|
186
|
+
*
|
|
187
|
+
* 1. The collection its `collection` front matter field names, when that collection exists.
|
|
188
|
+
* 2. Its own collection, when the page is a collection index page or an `index.md`, even
|
|
189
|
+
* when that collection is empty.
|
|
190
|
+
* 3. Otherwise the nearest collection with pages that the page belongs to: `/blog/post` reads
|
|
191
|
+
* `/blog`, and a top-level page reads the root collection.
|
|
192
|
+
*
|
|
193
|
+
* Pagination pages after the first read the collection of the first page. Generated pages
|
|
194
|
+
* read only the collection their template names; expansion sets it in their front matter.
|
|
195
|
+
*
|
|
196
|
+
* @param page - The page
|
|
197
|
+
* @param allPages - All pages of the build
|
|
198
|
+
* @returns The index page URL of the collection, or undefined when the page has none
|
|
199
|
+
*/
|
|
200
|
+
export function resolveCollectionPath(page, allPages) {
|
|
201
|
+
const index = getCollections(allPages);
|
|
202
|
+
const named = findCollectionPath(index, page.frontMatter.collection);
|
|
203
|
+
if (named !== undefined) {
|
|
204
|
+
return named;
|
|
205
|
+
}
|
|
206
|
+
if (page.generated) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if ((page.pagination?.pageNumber ?? 1) > 1) {
|
|
210
|
+
const first = getFirstPaginationPage(page, allPages);
|
|
211
|
+
return first && !isVirtualPage(first) ? resolveCollectionPath(first, allPages) : undefined;
|
|
212
|
+
}
|
|
213
|
+
if (index.byPath.has(page.url)) {
|
|
214
|
+
return page.url;
|
|
215
|
+
}
|
|
216
|
+
// A page always has pages in the collections it belongs to; only a [param].md template can
|
|
217
|
+
// sit in an empty one, and site/blog/tags/[tag].md reads /blog even with a tags/index.md
|
|
218
|
+
return collectionsContaining(page.url, (path) => (index.byPath.get(path)?.pages.length ?? 0) > 0).at(-1);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Returns the front matter fields of the collection summary.
|
|
222
|
+
*
|
|
223
|
+
* @param config - Stati configuration
|
|
224
|
+
* @returns `collections.summaryFields`, or the default fields
|
|
225
|
+
*/
|
|
226
|
+
export function resolveSummaryFields(config) {
|
|
227
|
+
const fields = config.collections?.summaryFields;
|
|
228
|
+
if (!Array.isArray(fields)) {
|
|
229
|
+
return DEFAULT_SUMMARY_FIELDS;
|
|
230
|
+
}
|
|
231
|
+
return fields.filter((field) => typeof field === 'string' && field !== '');
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Hashes what a listing shows of a list of pages: each page's URL, publish date and summary
|
|
235
|
+
* fields, in list order. The Markdown body only counts when `fields` includes `content`.
|
|
236
|
+
*
|
|
237
|
+
* @param pages - The listed pages
|
|
238
|
+
* @param fields - Front matter fields to include
|
|
239
|
+
* @returns A `sha256-…` hash
|
|
240
|
+
*/
|
|
241
|
+
export function computePagesSummaryHash(pages, fields) {
|
|
242
|
+
return computeContentHash('', {
|
|
243
|
+
fields: [...fields],
|
|
244
|
+
pages: pages.map((page) => [
|
|
245
|
+
page.url,
|
|
246
|
+
toIsoDate(page.publishedAt),
|
|
247
|
+
...fields.map((field) => field === 'content' ? page.content : (page.frontMatter[field] ?? null)),
|
|
248
|
+
]),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function toIsoDate(date) {
|
|
252
|
+
return date instanceof Date && !Number.isNaN(date.getTime()) ? date.toISOString() : null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Creates the summary hasher of a build. Call it after the `beforeAll` hook, which can change
|
|
256
|
+
* the front matter the summaries include.
|
|
257
|
+
*
|
|
258
|
+
* @param allPages - All pages of the build
|
|
259
|
+
* @param fields - Summary fields
|
|
260
|
+
* @returns The hasher
|
|
261
|
+
*/
|
|
262
|
+
export function createSummaryHasher(allPages, fields) {
|
|
263
|
+
const memo = new Map();
|
|
264
|
+
return {
|
|
265
|
+
collection(path) {
|
|
266
|
+
let hash = memo.get(path);
|
|
267
|
+
if (hash === undefined) {
|
|
268
|
+
const data = getCollections(allPages).byPath.get(path);
|
|
269
|
+
hash = data ? computePagesSummaryHash(data.pages, fields) : 'none';
|
|
270
|
+
memo.set(path, hash);
|
|
271
|
+
}
|
|
272
|
+
return hash;
|
|
273
|
+
},
|
|
274
|
+
pages: (pages) => computePagesSummaryHash(pages, fields),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const COLLECTION_KEYS = ['pages', 'children', 'recentPages', 'pagesByTag', 'metadata'];
|
|
278
|
+
/**
|
|
279
|
+
* Wraps collection data for one render. Reading any of its properties sets `reads.collection`,
|
|
280
|
+
* so the cache knows the page depends on the collection. Arrays and objects are copied on
|
|
281
|
+
* first read, so a template that sorts or changes them cannot affect other pages, which share
|
|
282
|
+
* the collection data within a build.
|
|
283
|
+
*
|
|
284
|
+
* The properties are accessors on a plain object, not a Proxy, so spreading the template
|
|
285
|
+
* context (as callable partials do) passes the view along without reading it.
|
|
286
|
+
*
|
|
287
|
+
* @param data - The shared collection data
|
|
288
|
+
* @param reads - The render's read flags
|
|
289
|
+
* @returns The view for `stati.collection`
|
|
290
|
+
*/
|
|
291
|
+
export function createCollectionView(data, reads) {
|
|
292
|
+
const view = {};
|
|
293
|
+
for (const key of COLLECTION_KEYS) {
|
|
294
|
+
let value;
|
|
295
|
+
let copied = false;
|
|
296
|
+
Object.defineProperty(view, key, {
|
|
297
|
+
enumerable: true,
|
|
298
|
+
configurable: true,
|
|
299
|
+
get() {
|
|
300
|
+
reads.collection = true;
|
|
301
|
+
if (!copied) {
|
|
302
|
+
value = copyCollectionValue(data[key]);
|
|
303
|
+
copied = true;
|
|
304
|
+
}
|
|
305
|
+
return value;
|
|
306
|
+
},
|
|
307
|
+
set(next) {
|
|
308
|
+
reads.collection = true;
|
|
309
|
+
value = next;
|
|
310
|
+
copied = true;
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return view;
|
|
315
|
+
}
|
|
316
|
+
function copyCollectionValue(value) {
|
|
317
|
+
if (Array.isArray(value)) {
|
|
318
|
+
return [...value];
|
|
319
|
+
}
|
|
320
|
+
const copy = Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, Array.isArray(entry) ? [...entry] : entry]));
|
|
321
|
+
// pagesByTag has no prototype, and neither has its copy, so any tag stays a plain key
|
|
322
|
+
return Object.setPrototypeOf(copy, Object.getPrototypeOf(value));
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Copies a page's pagination data for `stati.pagination`, so a template that changes the
|
|
326
|
+
* lists cannot change what the cache hashes.
|
|
327
|
+
*
|
|
328
|
+
* @param data - The page's pagination data
|
|
329
|
+
* @returns A copy with its own arrays
|
|
330
|
+
*/
|
|
331
|
+
export function copyPaginationData(data) {
|
|
332
|
+
return { ...data, items: [...data.items], urls: [...data.urls] };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Copies a page's generated data for `stati.generated`.
|
|
336
|
+
*
|
|
337
|
+
* @param data - The page's generated data
|
|
338
|
+
* @returns A copy with its own items array
|
|
339
|
+
*/
|
|
340
|
+
export function copyGeneratedData(data) {
|
|
341
|
+
return { ...data, items: [...data.items] };
|
|
342
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/core/content.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/core/content.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,CAiDtB"}
|
package/dist/core/content.js
CHANGED
|
@@ -39,7 +39,8 @@ export async function loadContent(config, includeDrafts) {
|
|
|
39
39
|
if (frontMatter.draft && !includeDrafts) {
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// POSIX separators, so a page two folders deep gets /a/b/page on Windows too
|
|
43
|
+
const relativePath = relative(contentDir, file).replace(/\\/g, '/');
|
|
43
44
|
const slug = computeSlug(relativePath);
|
|
44
45
|
const url = computeUrl(slug, frontMatter);
|
|
45
46
|
const page = {
|
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAiC7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAmZD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA4hBxF"}
|
package/dist/core/dev.js
CHANGED
|
@@ -8,9 +8,7 @@ import { invalidate } from './invalidate.js';
|
|
|
8
8
|
import { loadConfig } from '../config/loader.js';
|
|
9
9
|
import { clearTemplateEngineCache } from './templates.js';
|
|
10
10
|
import { clearMarkdownProcessorCache } from './markdown.js';
|
|
11
|
-
import { loadCacheManifest, saveCacheManifest,
|
|
12
|
-
import { loadContent } from './content.js';
|
|
13
|
-
import { buildNavigation } from './navigation.js';
|
|
11
|
+
import { loadCacheManifest, saveCacheManifest, DevServerLockManager } from './isg/index.js';
|
|
14
12
|
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, isEnabledEnvFlag, refreshCssOutputWatcher, normalizePathForComparison, isPathWithinDirectory, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
|
|
15
13
|
import { setEnv, getEnv } from '../env.js';
|
|
16
14
|
import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION, DEFAULT_OUT_DIR, } from '../constants.js';
|
|
@@ -318,67 +316,18 @@ async function handleTemplateChange(templatePath, configPath, logger) {
|
|
|
318
316
|
}
|
|
319
317
|
}
|
|
320
318
|
/**
|
|
321
|
-
* Handles markdown file changes
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
319
|
+
* Handles markdown file changes (added, edited or deleted pages) with an incremental build.
|
|
320
|
+
* ISG decides what re-renders: the changed page, the pages that list it through a collection,
|
|
321
|
+
* and the pages that read the navigation when the change altered it. A deleted page's output
|
|
322
|
+
* is removed by the build.
|
|
325
323
|
*/
|
|
326
324
|
async function handleMarkdownChange(_markdownPath, configPath, logger) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await build({
|
|
334
|
-
logger,
|
|
335
|
-
force: false,
|
|
336
|
-
clean: false,
|
|
337
|
-
...(configPath && { configPath }),
|
|
338
|
-
});
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
// Load config and content to rebuild navigation tree
|
|
342
|
-
const config = await loadConfig(configPath);
|
|
343
|
-
const pages = await loadContent(config);
|
|
344
|
-
const newNavigation = buildNavigation(pages);
|
|
345
|
-
const newNavigationHash = computeNavigationHash(newNavigation);
|
|
346
|
-
// Compare navigation hashes
|
|
347
|
-
if (newNavigationHash !== cacheManifest.navigationHash) {
|
|
348
|
-
// Navigation structure changed - clear cache and force full rebuild
|
|
349
|
-
logger.status('Navigation structure changed, performing full rebuild...');
|
|
350
|
-
// Force rebuild bypasses ISG cache entirely
|
|
351
|
-
await build({
|
|
352
|
-
logger,
|
|
353
|
-
force: true, // Force rebuild to bypass cache
|
|
354
|
-
clean: false,
|
|
355
|
-
...(configPath && { configPath }),
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
// Navigation unchanged - use incremental rebuild for content changes
|
|
360
|
-
await build({
|
|
361
|
-
logger,
|
|
362
|
-
force: false,
|
|
363
|
-
clean: false,
|
|
364
|
-
...(configPath && { configPath }),
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch (_error) {
|
|
369
|
-
try {
|
|
370
|
-
// Fallback to full rebuild
|
|
371
|
-
await build({
|
|
372
|
-
logger,
|
|
373
|
-
force: false,
|
|
374
|
-
clean: false,
|
|
375
|
-
...(configPath && { configPath }),
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
catch (fallbackError) {
|
|
379
|
-
throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
325
|
+
await build({
|
|
326
|
+
logger,
|
|
327
|
+
force: false,
|
|
328
|
+
clean: false,
|
|
329
|
+
...(configPath && { configPath }),
|
|
330
|
+
});
|
|
382
331
|
}
|
|
383
332
|
export async function createDevServer(options = {}) {
|
|
384
333
|
const logger = options.logger ?? createFallbackLogger();
|