@stati/core 1.28.0 → 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/core/build.d.ts.map +1 -1
- package/dist/core/build.js +122 -34
- 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 +15 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +44 -6
- package/dist/core/isg/hash.d.ts +4 -2
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +16 -2
- package/dist/core/isg/index.d.ts +2 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -1
- package/dist/core/isg/manifest.js +6 -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/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 +3 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +67 -127
- package/dist/core/utils/fs.utils.d.ts +8 -0
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +13 -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/index.d.ts +1 -1
- package/dist/index.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/types/config.d.ts +55 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +129 -1
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +12 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { FrontMatter, Logger, PageModel, PaginationData, StatiConfig } from '../types/index.js';
|
|
2
|
+
/** URL segment before the page number when `paginate.path` is not set */
|
|
3
|
+
export declare const DEFAULT_PAGINATION_PATH = "page";
|
|
4
|
+
/**
|
|
5
|
+
* Resolved pagination settings.
|
|
6
|
+
*/
|
|
7
|
+
interface PaginationSettings {
|
|
8
|
+
size: number;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns the parameter name of a `[param].md` template, or undefined for other pages.
|
|
13
|
+
*
|
|
14
|
+
* @param page - A loaded page
|
|
15
|
+
* @returns The name between the brackets
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTemplateParam(page: PageModel): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Replaces `{param}` with the label in a string. `{{param}}` produces the literal `{param}`,
|
|
20
|
+
* and placeholders of other names are left as they are.
|
|
21
|
+
*
|
|
22
|
+
* @param text - The string to fill in
|
|
23
|
+
* @param param - The template's parameter name, such as `tag`
|
|
24
|
+
* @param label - The value, such as `CSS`
|
|
25
|
+
* @returns The string with its placeholders replaced
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* replacePlaceholders('Posts tagged {tag}', 'tag', 'CSS'); // 'Posts tagged CSS'
|
|
30
|
+
* replacePlaceholders('Use {{tag}} in titles', 'tag', 'CSS'); // 'Use {tag} in titles'
|
|
31
|
+
* replacePlaceholders('{author} on {tag}', 'tag', 'CSS'); // '{author} on CSS'
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function replacePlaceholders(text: string, param: string, label: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Copies a template's front matter for one generated page, with `{param}` placeholders filled
|
|
37
|
+
* in. `generate` and `permalink` are copied as they are: they configure the template, and
|
|
38
|
+
* `permalink` takes the key through `[param]` instead.
|
|
39
|
+
*
|
|
40
|
+
* @param frontMatter - The template's front matter
|
|
41
|
+
* @param param - The template's parameter name
|
|
42
|
+
* @param label - The value for the page
|
|
43
|
+
* @returns The page's front matter
|
|
44
|
+
*/
|
|
45
|
+
export declare function substituteFrontMatter(frontMatter: FrontMatter, param: string, label: string): FrontMatter;
|
|
46
|
+
/**
|
|
47
|
+
* URL of page `pageNumber` of a listing whose first page is `baseUrl`. Later pages follow the
|
|
48
|
+
* first page's trailing-slash style: `/blog` → `/blog/page/2`, `/blog/` → `/blog/page/2/`,
|
|
49
|
+
* `/` → `/page/2`.
|
|
50
|
+
*
|
|
51
|
+
* @param baseUrl - URL of the first page
|
|
52
|
+
* @param path - URL segment before the page number
|
|
53
|
+
* @param pageNumber - Page number, starting at 1
|
|
54
|
+
* @returns The page URL
|
|
55
|
+
*/
|
|
56
|
+
export declare function getPaginationUrl(baseUrl: string, path: string, pageNumber: number): string;
|
|
57
|
+
/**
|
|
58
|
+
* Builds the pagination data of every page of a listing.
|
|
59
|
+
*
|
|
60
|
+
* @param baseUrl - URL of the first page
|
|
61
|
+
* @param items - Everything the listing shows, in order
|
|
62
|
+
* @param settings - Page size and URL segment
|
|
63
|
+
* @returns One entry per page; there is always at least one
|
|
64
|
+
*/
|
|
65
|
+
export declare function paginateItems(baseUrl: string, items: readonly PageModel[], settings: PaginationSettings): PaginationData[];
|
|
66
|
+
/**
|
|
67
|
+
* Turns the pages loaded from Markdown into the pages of a build:
|
|
68
|
+
*
|
|
69
|
+
* - Each `[param].md` template is removed and replaced by one page per key: per value of
|
|
70
|
+
* `generate.from` in its collection, and per `collections.generate` entry that names it.
|
|
71
|
+
* A template that produces no keys produces no output.
|
|
72
|
+
* - Every page with `paginate`, and every generated page whose template has
|
|
73
|
+
* `generate.paginate`, gets one more page per additional slice of its items.
|
|
74
|
+
*
|
|
75
|
+
* Generated and pagination pages are virtual: they are never collection members and are left
|
|
76
|
+
* out of navigation, search and RSS unless they opt in. A virtual page whose URL or output
|
|
77
|
+
* file is already taken is skipped with a warning.
|
|
78
|
+
*
|
|
79
|
+
* @param loaded - Pages loaded from Markdown files
|
|
80
|
+
* @param config - Stati configuration
|
|
81
|
+
* @param logger - Logger for warnings
|
|
82
|
+
* @returns The pages to build: loaded pages without templates, then virtual pages
|
|
83
|
+
*/
|
|
84
|
+
export declare function expandPages(loaded: PageModel[], config: StatiConfig, logger: Logger): Promise<PageModel[]>;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=expand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../src/core/expand.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EAGX,MAAM,EACN,SAAS,EACT,cAAc,EACd,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAmB3B,yEAAyE;AACzE,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAE9C;;GAEG;AACH,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,WAAW,CASb;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAU1F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,QAAQ,EAAE,kBAAkB,GAC3B,cAAc,EAAE,CAclB;AA6FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,SAAS,EAAE,CAAC,CA+NtB"}
|