@stati/core 1.28.0 → 1.29.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/README.md +6 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +192 -71
- 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 +32 -93
- 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/preview.d.ts.map +1 -1
- package/dist/core/preview.js +21 -32
- 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/bundle-matching.utils.d.ts +8 -8
- package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -1
- package/dist/core/utils/bundle-matching.utils.js +10 -10
- 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 +6 -6
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +5 -5
- 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/server.utils.d.ts +38 -3
- package/dist/core/utils/server.utils.d.ts.map +1 -1
- package/dist/core/utils/server.utils.js +132 -44
- 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/core/utils/typescript.utils.d.ts +27 -0
- package/dist/core/utils/typescript.utils.d.ts.map +1 -1
- package/dist/core/utils/typescript.utils.js +34 -1
- 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 +27 -7
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +55 -12
- 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 +61 -4
- 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
package/README.md
CHANGED
|
@@ -166,7 +166,8 @@ Customize layouts with [Eta templates](https://eta.js.org):
|
|
|
166
166
|
|
|
167
167
|
Only rebuild what changed:
|
|
168
168
|
|
|
169
|
-
- Smart caching based on hashes of each page's content and templates
|
|
169
|
+
- Smart caching based on hashes of each page's content and templates, and of the collection and navigation data it reads
|
|
170
|
+
- Removal of pages that no longer exist, such as deleted posts and unused tag pages
|
|
170
171
|
- TTL-based refresh for dynamic content
|
|
171
172
|
- Tag-based invalidation for related content
|
|
172
173
|
|
|
@@ -183,6 +184,10 @@ await invalidate('path:/posts');
|
|
|
183
184
|
await invalidate('age:3months');
|
|
184
185
|
```
|
|
185
186
|
|
|
187
|
+
#### Collections, Pagination and Generated Pages
|
|
188
|
+
|
|
189
|
+
A folder with an `index.md` is a collection: templates read its pages as `stati.collection`, sorted by date and grouped by tag. `paginate: 10` in front matter splits a listing into `/blog`, `/blog/page/2` and so on (`stati.pagination`), and a `[tag].md` template becomes one page per tag (`stati.generated`). See [Collections and generated pages](https://stati.build/core-concepts/collections).
|
|
190
|
+
|
|
186
191
|
#### Built-In SEO
|
|
187
192
|
|
|
188
193
|
SEO optimization works automatically with zero configuration:
|
package/dist/core/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAyEA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EAQP,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, } from './utils/index.js';
|
|
1
|
+
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, removeStaleBundleFiles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, } from './utils/index.js';
|
|
2
2
|
import { join, dirname, relative, posix } from 'node:path';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { loadConfig } from '../config/loader.js';
|
|
5
5
|
import { loadContent } from './content.js';
|
|
6
|
+
import { expandPages } from './expand.js';
|
|
7
|
+
import { createSummaryHasher, resolveCollectionPath, resolveSummaryFields, } from './collections.js';
|
|
6
8
|
import { createMarkdownProcessor, renderMarkdown, extractToc } from './markdown.js';
|
|
7
9
|
import { createTemplateEngine, renderPage } from './templates.js';
|
|
8
10
|
import { buildNavigation } from './navigation.js';
|
|
9
|
-
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
11
|
+
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
10
12
|
import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
|
|
11
13
|
import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
|
|
12
|
-
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, } from '../search/index.js';
|
|
14
|
+
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, removeStaleSearchIndexes, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
|
|
13
15
|
import { getEnv, isDevelopment } from '../env.js';
|
|
14
16
|
import { DEFAULT_OUT_DIR } from '../constants.js';
|
|
15
17
|
import { ImageRuntime } from '../images/runtime.js';
|
|
@@ -180,12 +182,18 @@ async function setupCacheAndManifest(cacheDir) {
|
|
|
180
182
|
return { manifest };
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
|
-
* Loads content and builds navigation.
|
|
185
|
+
* Loads content, expands generated and paginated pages, and builds navigation.
|
|
184
186
|
*/
|
|
185
187
|
async function loadContentAndBuildNavigation(config, options, logger) {
|
|
186
|
-
// Load all content
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Load all content, then replace [param].md templates with their pages and add the
|
|
189
|
+
// pages of paginated listings. Navigation, SEO, sitemap and search see the result.
|
|
190
|
+
const loaded = await loadContent(config, options.includeDrafts);
|
|
191
|
+
const pages = await expandPages(loaded, config, logger);
|
|
192
|
+
const virtualCount = pages.filter((page) => isVirtualPage(page)).length;
|
|
193
|
+
logger.status(`Found ${pages.length - virtualCount} pages`);
|
|
194
|
+
if (virtualCount > 0) {
|
|
195
|
+
logger.status(`Generated ${virtualCount} more from templates and pagination`);
|
|
196
|
+
}
|
|
189
197
|
// Build navigation from pages
|
|
190
198
|
if (logger.step) {
|
|
191
199
|
console.log(); // Add spacing before navigation step
|
|
@@ -202,34 +210,89 @@ async function loadContentAndBuildNavigation(config, options, logger) {
|
|
|
202
210
|
// Create processors
|
|
203
211
|
const md = await createMarkdownProcessor(config);
|
|
204
212
|
const eta = createTemplateEngine(config);
|
|
205
|
-
return { pages, navigation, md, eta, navigationHash };
|
|
213
|
+
return { pages, markdownFiles: loaded.length, navigation, md, eta, navigationHash };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Generates the search index from the page models and writes it before any page renders,
|
|
217
|
+
* returning the file name pages link to. Every page links to the index, and the name is part
|
|
218
|
+
* of each page's cache hash, so a production build names a hashed file after the index
|
|
219
|
+
* content: a build that leaves the indexed text alone keeps the name and its cached pages.
|
|
220
|
+
* Development keeps one name, so a content edit does not re-render every page. Writing the
|
|
221
|
+
* index first means it is not held in memory while the pages render.
|
|
222
|
+
*
|
|
223
|
+
* @returns The index file name, or undefined when search is disabled
|
|
224
|
+
*/
|
|
225
|
+
async function writeSearchIndexBeforeRendering(pages, config, md, outDir, logger, recorder) {
|
|
226
|
+
if (config.search?.enabled !== true) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const isDevMode = isDevelopment();
|
|
230
|
+
const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
|
|
231
|
+
// TOC entries and markdown content, the same data a render produces, without rendering.
|
|
232
|
+
// Sorted by URL: content discovery returns files in a different order on each run, which
|
|
233
|
+
// would change the index content, and with it the name, on every build.
|
|
234
|
+
const tocEnabled = config.markdown?.toc !== false;
|
|
235
|
+
const byUrl = (a, b) => a.url.localeCompare(b.url) || a.sourcePath.localeCompare(b.sourcePath);
|
|
236
|
+
const searchablePages = pages
|
|
237
|
+
.filter((page) => !isGeneratedPageExcludedFromSearch(page))
|
|
238
|
+
.sort(byUrl)
|
|
239
|
+
.map((page) => ({
|
|
240
|
+
page,
|
|
241
|
+
toc: tocEnabled ? extractToc(page.content, md) : [],
|
|
242
|
+
markdownContent: page.content,
|
|
243
|
+
}));
|
|
244
|
+
const index = generateSearchIndex(searchablePages, config.search);
|
|
245
|
+
// A production hash covers the documents and schema version, not generatedAt, which
|
|
246
|
+
// changes on every build; serializing them is skipped when the name has no hash
|
|
247
|
+
const hashInput = isDevMode
|
|
248
|
+
? 'dev'
|
|
249
|
+
: config.search.hashFilename !== false
|
|
250
|
+
? JSON.stringify({ version: index.version, documents: index.documents })
|
|
251
|
+
: undefined;
|
|
252
|
+
const filename = computeSearchIndexFilename(config.search, hashInput);
|
|
253
|
+
endSearchIndexSpan();
|
|
254
|
+
// Pages still link to the name when nothing is searchable, but no file is written
|
|
255
|
+
if (searchablePages.length === 0) {
|
|
256
|
+
return filename;
|
|
257
|
+
}
|
|
258
|
+
logger.info('');
|
|
259
|
+
logger.info(`Generating search index to ${filename}`);
|
|
260
|
+
const writeStart = performance.now();
|
|
261
|
+
// In dev mode, skip write if content hasn't changed (template-only changes)
|
|
262
|
+
const metadata = await writeSearchIndex(index, outDir, filename, isDevMode);
|
|
263
|
+
recorder.recordPhase('searchIndexWriteMs', performance.now() - writeStart);
|
|
264
|
+
logger.success(`Generated search index with ${metadata.documentCount} documents`);
|
|
265
|
+
return filename;
|
|
206
266
|
}
|
|
207
267
|
/**
|
|
208
268
|
* Output file and ISG cache key of a page.
|
|
209
269
|
*/
|
|
210
270
|
function getPageOutput(page, outDir) {
|
|
211
|
-
|
|
212
|
-
if (page.url === '/') {
|
|
213
|
-
outputPath = join(outDir, 'index.html');
|
|
214
|
-
}
|
|
215
|
-
else if (page.url.endsWith('/')) {
|
|
216
|
-
outputPath = join(outDir, page.url, 'index.html');
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
outputPath = join(outDir, `${page.url}.html`);
|
|
220
|
-
}
|
|
271
|
+
const outputPath = join(outDir, getPageOutputFile(page.url));
|
|
221
272
|
// Cache key is the output path relative to outDir
|
|
222
273
|
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
223
274
|
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
224
275
|
return { outputPath, cacheKey };
|
|
225
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Hashes a value for the render inputs, or returns a value that never matches when it
|
|
279
|
+
* cannot be serialized, so the page rebuilds instead of keeping stale output.
|
|
280
|
+
*/
|
|
281
|
+
function hashableData(value) {
|
|
282
|
+
try {
|
|
283
|
+
JSON.stringify(value);
|
|
284
|
+
return value ?? null;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return `unserializable:${Date.now()}:${Math.random()}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
226
290
|
/**
|
|
227
291
|
* Processes pages with ISG caching logic.
|
|
228
292
|
*/
|
|
229
|
-
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder,
|
|
293
|
+
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
|
|
230
294
|
let cacheHits = 0;
|
|
231
295
|
let cacheMisses = 0;
|
|
232
|
-
const searchablePages = [];
|
|
233
296
|
// Build context
|
|
234
297
|
const buildContext = { config, pages };
|
|
235
298
|
// Run beforeAll hook
|
|
@@ -238,6 +301,20 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
238
301
|
await config.hooks.beforeAll(buildContext);
|
|
239
302
|
endHookBeforeAll();
|
|
240
303
|
}
|
|
304
|
+
// Collection summaries, hashed after beforeAll because the hook can change front matter
|
|
305
|
+
const summaries = createSummaryHasher(pages, resolveSummaryFields(config));
|
|
306
|
+
// Generated after beforeAll for the same reason; pages render with its file name
|
|
307
|
+
const searchIndexFilename = await writeSearchIndexBeforeRendering(pages, config, md, outDir, logger, recorder);
|
|
308
|
+
// The pages of a listing share one array of page URLs, hashed once for all of them
|
|
309
|
+
const urlListHashes = new WeakMap();
|
|
310
|
+
const hashUrlList = (urls) => {
|
|
311
|
+
let hash = urlListHashes.get(urls);
|
|
312
|
+
if (hash === undefined) {
|
|
313
|
+
hash = computeContentHash('', { urls });
|
|
314
|
+
urlListHashes.set(urls, hash);
|
|
315
|
+
}
|
|
316
|
+
return hash;
|
|
317
|
+
};
|
|
241
318
|
// Render each page with progress tracking and ISG
|
|
242
319
|
if (logger.step) {
|
|
243
320
|
logger.step(2, 3, 'Rendering pages');
|
|
@@ -273,18 +350,56 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
273
350
|
searchIndexPath: `/${searchIndexFilename}`,
|
|
274
351
|
}),
|
|
275
352
|
};
|
|
276
|
-
|
|
353
|
+
// Collection the page sees as stati.collection; a page whose collection appears,
|
|
354
|
+
// disappears or changes rebuilds even when it never read it
|
|
355
|
+
const collectionPath = resolveCollectionPath(page, pages);
|
|
356
|
+
const staticRenderInputs = {
|
|
277
357
|
...assets,
|
|
278
358
|
bundleAutoInject: config.typescript?.autoInject !== false,
|
|
279
359
|
searchAutoInject: config.search?.autoInjectMetaTag !== false,
|
|
280
360
|
// Omitted with images: false, so the hash matches builds without the image pipeline
|
|
281
361
|
...(imageFingerprint && { images: imageFingerprint }),
|
|
362
|
+
collection: collectionPath ?? null,
|
|
363
|
+
// A listing page depends on its own slice and on the page count, which every page
|
|
364
|
+
// shows in its numbered links
|
|
365
|
+
...(page.pagination && {
|
|
366
|
+
pagination: {
|
|
367
|
+
pageNumber: page.pagination.pageNumber,
|
|
368
|
+
totalPages: page.pagination.totalPages,
|
|
369
|
+
urls: hashUrlList(page.pagination.urls),
|
|
370
|
+
items: summaries.pages(page.pagination.items),
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
// A generated page depends on its own items only, not on the whole collection
|
|
374
|
+
...(page.generated && {
|
|
375
|
+
generated: {
|
|
376
|
+
key: page.generated.key,
|
|
377
|
+
label: page.generated.label,
|
|
378
|
+
template: page.generated.template,
|
|
379
|
+
items: summaries.pages(page.generated.items),
|
|
380
|
+
data: hashableData(page.generated.data),
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
};
|
|
384
|
+
// The data a render read: the collection summary and the navigation tree
|
|
385
|
+
const renderInputsHashFor = (reads) => computeContentHash('', {
|
|
386
|
+
...staticRenderInputs,
|
|
387
|
+
...(reads?.collection && {
|
|
388
|
+
collectionSummary: collectionPath !== undefined ? summaries.collection(collectionPath) : 'none',
|
|
389
|
+
}),
|
|
390
|
+
...(reads?.nav && { navigation: navigationHash }),
|
|
282
391
|
});
|
|
392
|
+
// Entries written before Stati recorded what pages read are rebuilt once to record it
|
|
393
|
+
const readsKnown = existingEntry !== undefined && hasRecordedReads(existingEntry);
|
|
394
|
+
const renderInputsHash = renderInputsHashFor(readsKnown
|
|
395
|
+
? { collection: existingEntry.readsCollection, nav: existingEntry.readsNav }
|
|
396
|
+
: undefined);
|
|
283
397
|
// Check if we should rebuild this page (considering ISG logic)
|
|
284
398
|
const shouldRebuildStart = performance.now();
|
|
285
399
|
const shouldRebuild = options.force ||
|
|
286
400
|
config.isg?.enabled === false ||
|
|
287
401
|
!(await pathExists(outputPath)) ||
|
|
402
|
+
(existingEntry !== undefined && !readsKnown) ||
|
|
288
403
|
// A cached page whose generated images are gone is rebuilt to regenerate them
|
|
289
404
|
(imageRuntime !== null && !imageRuntime.outputsExist(existingEntry?.images)) ||
|
|
290
405
|
(await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
|
|
@@ -298,13 +413,6 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
298
413
|
}
|
|
299
414
|
// Record page timing for cached pages (0ms render time)
|
|
300
415
|
recorder.recordPageTiming(page.url, 0, true);
|
|
301
|
-
// Collect searchable page data for cached pages if search is enabled
|
|
302
|
-
// Extract TOC without full HTML rendering for efficiency
|
|
303
|
-
if (config.search?.enabled === true) {
|
|
304
|
-
const tocEnabled = config.markdown?.toc !== false;
|
|
305
|
-
const toc = tocEnabled ? extractToc(page.content, md) : [];
|
|
306
|
-
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
307
|
-
}
|
|
308
416
|
continue;
|
|
309
417
|
}
|
|
310
418
|
// Cache miss - need to rebuild
|
|
@@ -357,10 +465,17 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
357
465
|
if (config.typescript?.autoInject !== false && assets.bundlePaths.length > 0) {
|
|
358
466
|
finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
|
|
359
467
|
}
|
|
360
|
-
// Start the image jobs the final HTML needs and record its image files for the cache
|
|
361
|
-
|
|
468
|
+
// Start the image jobs the final HTML needs and record its image files for the cache,
|
|
469
|
+
// along with what the render read, which decides the hash it is stored under
|
|
470
|
+
const committedImages = pageImages && imageRuntime
|
|
362
471
|
? imageRuntime.commitPage(pageImages, finalHtml, { outputPath, cacheKey })
|
|
363
472
|
: undefined;
|
|
473
|
+
const renderedAssets = {
|
|
474
|
+
assetDeps: committedImages?.assetDeps ?? [],
|
|
475
|
+
images: committedImages?.images ?? [],
|
|
476
|
+
reads: renderResult.reads,
|
|
477
|
+
};
|
|
478
|
+
const renderedInputsHash = renderInputsHashFor(renderResult.reads);
|
|
364
479
|
const renderTime = Math.round(performance.now() - startTime);
|
|
365
480
|
// Record page timing for rendered pages (includes template count)
|
|
366
481
|
recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
|
|
@@ -370,7 +485,13 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
370
485
|
// In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
|
|
371
486
|
if (isDevMode) {
|
|
372
487
|
pendingWrites.push({ outputPath, content: finalHtml });
|
|
373
|
-
pendingCacheUpdates.push({
|
|
488
|
+
pendingCacheUpdates.push({
|
|
489
|
+
cacheKey,
|
|
490
|
+
page,
|
|
491
|
+
existingEntry,
|
|
492
|
+
renderInputsHash: renderedInputsHash,
|
|
493
|
+
renderedAssets,
|
|
494
|
+
});
|
|
374
495
|
}
|
|
375
496
|
else {
|
|
376
497
|
// Production mode: write immediately
|
|
@@ -382,19 +503,14 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
382
503
|
if (!options.skipCacheUpdate) {
|
|
383
504
|
const cacheEntryStart = performance.now();
|
|
384
505
|
if (existingEntry) {
|
|
385
|
-
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false,
|
|
506
|
+
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderedInputsHash, renderedAssets);
|
|
386
507
|
}
|
|
387
508
|
else {
|
|
388
|
-
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime,
|
|
509
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderedInputsHash, renderedAssets);
|
|
389
510
|
}
|
|
390
511
|
totalCacheEntryTime += performance.now() - cacheEntryStart;
|
|
391
512
|
}
|
|
392
513
|
}
|
|
393
|
-
// Collect searchable page data if search is enabled
|
|
394
|
-
// Uses TOC entries and markdown content instead of parsing rendered HTML
|
|
395
|
-
if (config.search?.enabled === true) {
|
|
396
|
-
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
397
|
-
}
|
|
398
514
|
// Run afterRender hook
|
|
399
515
|
if (config.hooks?.afterRender) {
|
|
400
516
|
const hookStart = performance.now();
|
|
@@ -440,7 +556,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
440
556
|
logger.showRenderingSummary();
|
|
441
557
|
}
|
|
442
558
|
}
|
|
443
|
-
return { cacheHits, cacheMisses,
|
|
559
|
+
return { cacheHits, cacheMisses, searchIndexFilename };
|
|
444
560
|
}
|
|
445
561
|
/**
|
|
446
562
|
* Copies static assets and returns the count.
|
|
@@ -567,14 +683,13 @@ async function buildInternal(options = {}) {
|
|
|
567
683
|
console.log(); // Add spacing before content loading
|
|
568
684
|
}
|
|
569
685
|
const endContentSpan = recorder.startSpan('contentDiscoveryMs');
|
|
570
|
-
const { pages, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
686
|
+
const { pages, markdownFiles, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
571
687
|
endContentSpan();
|
|
572
|
-
// Record content discovery counts.
|
|
573
|
-
//
|
|
574
|
-
// because loadContent() only processes *.md files, so all pages are markdown files.
|
|
688
|
+
// Record content discovery counts. Pages include the ones generated from [param].md
|
|
689
|
+
// templates and pagination; Markdown files include the templates.
|
|
575
690
|
recorder.increment('totalPages', pages.length);
|
|
576
|
-
recorder.increment('markdownFilesProcessed',
|
|
577
|
-
//
|
|
691
|
+
recorder.increment('markdownFilesProcessed', markdownFiles);
|
|
692
|
+
// Recorded in the manifest; pages that read the navigation also hash it into their inputs
|
|
578
693
|
manifest.navigationHash = navigationHash;
|
|
579
694
|
// Compile TypeScript if enabled
|
|
580
695
|
// In dev mode, skip compilation since esbuild watcher handles it
|
|
@@ -613,22 +728,29 @@ async function buildInternal(options = {}) {
|
|
|
613
728
|
});
|
|
614
729
|
endTsSpan();
|
|
615
730
|
}
|
|
616
|
-
// Pre-compute search index filename if search is enabled
|
|
617
|
-
// In dev mode, use a stable filename to simlplify testing and debugging
|
|
618
|
-
let searchIndexFilename;
|
|
619
|
-
if (config.search?.enabled) {
|
|
620
|
-
const buildId = isDevMode ? 'dev' : buildStartTime.toString();
|
|
621
|
-
searchIndexFilename = computeSearchIndexFilename(config.search, buildId);
|
|
622
|
-
}
|
|
623
731
|
// Process pages with ISG caching logic
|
|
624
732
|
if (logger.step) {
|
|
625
733
|
console.log(); // Add spacing before page processing
|
|
626
734
|
}
|
|
627
735
|
const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
|
|
628
736
|
const buildTime = new Date();
|
|
629
|
-
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder,
|
|
737
|
+
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, imageRuntime, navigationHash);
|
|
630
738
|
endPageRenderSpan();
|
|
631
|
-
const { cacheHits, cacheMisses,
|
|
739
|
+
const { cacheHits, cacheMisses, searchIndexFilename } = pageProcessingResult;
|
|
740
|
+
// Remove the output of pages that no longer exist, such as a deleted post or a tag no post
|
|
741
|
+
// uses anymore. Runs before static assets are copied, so a file in the static directory at
|
|
742
|
+
// the same path is copied afterwards.
|
|
743
|
+
const liveCacheKeys = new Set(pages.map((page) => getPageOutput(page, outDir).cacheKey));
|
|
744
|
+
const pruned = await pruneOrphanedPages(manifest, liveCacheKeys, outDir);
|
|
745
|
+
for (const failure of pruned.failed) {
|
|
746
|
+
logger.warning(`Could not remove the stale page ${failure.key}: ${failure.message}`);
|
|
747
|
+
}
|
|
748
|
+
if (pruned.removed.length > 0) {
|
|
749
|
+
const count = pruned.removed.length;
|
|
750
|
+
logger.info(count === 1
|
|
751
|
+
? 'Removed 1 page that no longer exists'
|
|
752
|
+
: `Removed ${count} pages that no longer exist`);
|
|
753
|
+
}
|
|
632
754
|
// Wait for image jobs; pages whose images failed are rewritten to show plain images
|
|
633
755
|
let imageFailures = [];
|
|
634
756
|
if (imageRuntime) {
|
|
@@ -646,23 +768,6 @@ async function buildInternal(options = {}) {
|
|
|
646
768
|
logger.info(`Images: ${generated} generated, ${cached} reused${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
647
769
|
}
|
|
648
770
|
}
|
|
649
|
-
// Generate search index if enabled
|
|
650
|
-
// Uses markdown content and TOC entries
|
|
651
|
-
let searchIndexMetadata;
|
|
652
|
-
if (config.search?.enabled && searchablePages.length > 0 && searchIndexFilename) {
|
|
653
|
-
logger.info('');
|
|
654
|
-
logger.info(`Generating search index to ${searchIndexFilename}`);
|
|
655
|
-
const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
|
|
656
|
-
const searchIndex = generateSearchIndex(searchablePages, config.search);
|
|
657
|
-
endSearchIndexSpan();
|
|
658
|
-
const writeStart = performance.now();
|
|
659
|
-
// In dev mode, skip write if content hasn't changed (template-only changes)
|
|
660
|
-
const skipIfUnchanged = isDevMode;
|
|
661
|
-
searchIndexMetadata = await writeSearchIndex(searchIndex, outDir, searchIndexFilename, skipIfUnchanged);
|
|
662
|
-
const writeTime = performance.now() - writeStart;
|
|
663
|
-
recorder.recordPhase('searchIndexWriteMs', writeTime);
|
|
664
|
-
logger.success(`Generated search index with ${searchIndexMetadata.documentCount} documents`);
|
|
665
|
-
}
|
|
666
771
|
// Record page rendering counts
|
|
667
772
|
recorder.increment('renderedPages', cacheMisses);
|
|
668
773
|
recorder.increment('cachedPages', cacheHits);
|
|
@@ -695,6 +800,22 @@ async function buildInternal(options = {}) {
|
|
|
695
800
|
.map((failure) => `Open Graph image failed for ${failure.label}: ${failure.message}`)
|
|
696
801
|
.join('\n'));
|
|
697
802
|
}
|
|
803
|
+
// Delete the bundles and search indexes earlier builds wrote under other names, now that
|
|
804
|
+
// every page links to this build's files. Before the static copy, so a file in the static
|
|
805
|
+
// directory with such a name is copied afterwards.
|
|
806
|
+
if (!isDevMode) {
|
|
807
|
+
if (config.typescript?.enabled) {
|
|
808
|
+
await removeStaleBundleFiles({
|
|
809
|
+
projectRoot: process.cwd(),
|
|
810
|
+
config: config.typescript,
|
|
811
|
+
outDir: config.outDir || DEFAULT_OUT_DIR,
|
|
812
|
+
compiled: compiledBundles,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if (config.search?.enabled && searchIndexFilename) {
|
|
816
|
+
await removeStaleSearchIndexes(outDir, config.search, searchIndexFilename);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
698
819
|
// Copy static assets and count them (skip for template-only changes in dev mode)
|
|
699
820
|
const endAssetSpan = recorder.startSpan('assetCopyMs');
|
|
700
821
|
let assetsCount = 0;
|
|
@@ -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"}
|