@stati/core 1.29.0 → 1.29.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAuEA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,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"}
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"}
@@ -1,4 +1,4 @@
1
- import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, } 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';
@@ -11,7 +11,7 @@ import { buildNavigation } from './navigation.js';
11
11
  import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
12
12
  import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
13
13
  import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
14
- import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
14
+ import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, removeStaleSearchIndexes, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
15
15
  import { getEnv, isDevelopment } from '../env.js';
16
16
  import { DEFAULT_OUT_DIR } from '../constants.js';
17
17
  import { ImageRuntime } from '../images/runtime.js';
@@ -212,6 +212,58 @@ async function loadContentAndBuildNavigation(config, options, logger) {
212
212
  const eta = createTemplateEngine(config);
213
213
  return { pages, markdownFiles: loaded.length, navigation, md, eta, navigationHash };
214
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;
266
+ }
215
267
  /**
216
268
  * Output file and ISG cache key of a page.
217
269
  */
@@ -238,10 +290,9 @@ function hashableData(value) {
238
290
  /**
239
291
  * Processes pages with ISG caching logic.
240
292
  */
241
- async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
293
+ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
242
294
  let cacheHits = 0;
243
295
  let cacheMisses = 0;
244
- const searchablePages = [];
245
296
  // Build context
246
297
  const buildContext = { config, pages };
247
298
  // Run beforeAll hook
@@ -252,6 +303,8 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
252
303
  }
253
304
  // Collection summaries, hashed after beforeAll because the hook can change front matter
254
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);
255
308
  // The pages of a listing share one array of page URLs, hashed once for all of them
256
309
  const urlListHashes = new WeakMap();
257
310
  const hashUrlList = (urls) => {
@@ -360,13 +413,6 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
360
413
  }
361
414
  // Record page timing for cached pages (0ms render time)
362
415
  recorder.recordPageTiming(page.url, 0, true);
363
- // Collect searchable page data for cached pages if search is enabled
364
- // Extract TOC without full HTML rendering for efficiency
365
- if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
366
- const tocEnabled = config.markdown?.toc !== false;
367
- const toc = tocEnabled ? extractToc(page.content, md) : [];
368
- searchablePages.push({ page, toc, markdownContent: page.content });
369
- }
370
416
  continue;
371
417
  }
372
418
  // Cache miss - need to rebuild
@@ -465,11 +511,6 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
465
511
  totalCacheEntryTime += performance.now() - cacheEntryStart;
466
512
  }
467
513
  }
468
- // Collect searchable page data if search is enabled
469
- // Uses TOC entries and markdown content instead of parsing rendered HTML
470
- if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
471
- searchablePages.push({ page, toc, markdownContent: page.content });
472
- }
473
514
  // Run afterRender hook
474
515
  if (config.hooks?.afterRender) {
475
516
  const hookStart = performance.now();
@@ -515,7 +556,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
515
556
  logger.showRenderingSummary();
516
557
  }
517
558
  }
518
- return { cacheHits, cacheMisses, searchablePages };
559
+ return { cacheHits, cacheMisses, searchIndexFilename };
519
560
  }
520
561
  /**
521
562
  * Copies static assets and returns the count.
@@ -687,22 +728,15 @@ async function buildInternal(options = {}) {
687
728
  });
688
729
  endTsSpan();
689
730
  }
690
- // Pre-compute search index filename if search is enabled
691
- // In dev mode, use a stable filename to simlplify testing and debugging
692
- let searchIndexFilename;
693
- if (config.search?.enabled) {
694
- const buildId = isDevMode ? 'dev' : buildStartTime.toString();
695
- searchIndexFilename = computeSearchIndexFilename(config.search, buildId);
696
- }
697
731
  // Process pages with ISG caching logic
698
732
  if (logger.step) {
699
733
  console.log(); // Add spacing before page processing
700
734
  }
701
735
  const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
702
736
  const buildTime = new Date();
703
- const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename, imageRuntime, navigationHash);
737
+ const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, imageRuntime, navigationHash);
704
738
  endPageRenderSpan();
705
- const { cacheHits, cacheMisses, searchablePages } = pageProcessingResult;
739
+ const { cacheHits, cacheMisses, searchIndexFilename } = pageProcessingResult;
706
740
  // Remove the output of pages that no longer exist, such as a deleted post or a tag no post
707
741
  // uses anymore. Runs before static assets are copied, so a file in the static directory at
708
742
  // the same path is copied afterwards.
@@ -734,23 +768,6 @@ async function buildInternal(options = {}) {
734
768
  logger.info(`Images: ${generated} generated, ${cached} reused${failed > 0 ? `, ${failed} failed` : ''}`);
735
769
  }
736
770
  }
737
- // Generate search index if enabled
738
- // Uses markdown content and TOC entries
739
- let searchIndexMetadata;
740
- if (config.search?.enabled && searchablePages.length > 0 && searchIndexFilename) {
741
- logger.info('');
742
- logger.info(`Generating search index to ${searchIndexFilename}`);
743
- const endSearchIndexSpan = recorder.startSpan('searchIndexGenerationMs');
744
- const searchIndex = generateSearchIndex(searchablePages, config.search);
745
- endSearchIndexSpan();
746
- const writeStart = performance.now();
747
- // In dev mode, skip write if content hasn't changed (template-only changes)
748
- const skipIfUnchanged = isDevMode;
749
- searchIndexMetadata = await writeSearchIndex(searchIndex, outDir, searchIndexFilename, skipIfUnchanged);
750
- const writeTime = performance.now() - writeStart;
751
- recorder.recordPhase('searchIndexWriteMs', writeTime);
752
- logger.success(`Generated search index with ${searchIndexMetadata.documentCount} documents`);
753
- }
754
771
  // Record page rendering counts
755
772
  recorder.increment('renderedPages', cacheMisses);
756
773
  recorder.increment('cachedPages', cacheHits);
@@ -783,6 +800,22 @@ async function buildInternal(options = {}) {
783
800
  .map((failure) => `Open Graph image failed for ${failure.label}: ${failure.message}`)
784
801
  .join('\n'));
785
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
+ }
786
819
  // Copy static assets and count them (skip for template-only changes in dev mode)
787
820
  const endAssetSpan = recorder.startSpan('assetCopyMs');
788
821
  let assetsCount = 0;
@@ -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;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"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAoC7D,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,CAmhBxF"}
package/dist/core/dev.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createServer } from 'node:http';
2
- import { join, extname } from 'node:path';
3
2
  import { readFile } from 'node:fs/promises';
4
3
  import { WebSocketServer } from 'ws';
5
4
  import chokidar from 'chokidar';
@@ -9,7 +8,7 @@ import { loadConfig } from '../config/loader.js';
9
8
  import { clearTemplateEngineCache } from './templates.js';
10
9
  import { clearMarkdownProcessorCache } from './markdown.js';
11
10
  import { loadCacheManifest, saveCacheManifest, DevServerLockManager } from './isg/index.js';
12
- import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, isEnabledEnvFlag, refreshCssOutputWatcher, normalizePathForComparison, isPathWithinDirectory, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
11
+ import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, resolveRequestFile, resolveNotFoundPage, getMimeType, parseRequestPath, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, isEnabledEnvFlag, refreshCssOutputWatcher, normalizePathForComparison, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
13
12
  import { setEnv, getEnv } from '../env.js';
14
13
  import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION, DEFAULT_OUT_DIR, } from '../constants.js';
15
14
  /**
@@ -367,30 +366,6 @@ export async function createDevServer(options = {}) {
367
366
  // Diagnostics toggles used for investigating dev-server rebuild slowdown.
368
367
  const disableCssWatcher = isEnabledEnvFlag(process.env.STATI_DEV_DISABLE_CSS_WATCHER);
369
368
  const disableWsReload = isEnabledEnvFlag(process.env.STATI_DEV_DISABLE_WS_RELOAD);
370
- /**
371
- * Gets MIME type for a file based on its extension
372
- */
373
- function getMimeType(filePath) {
374
- const ext = extname(filePath).toLowerCase();
375
- const mimeTypes = {
376
- '.html': 'text/html',
377
- '.js': 'application/javascript',
378
- '.css': 'text/css',
379
- '.json': 'application/json',
380
- '.png': 'image/png',
381
- '.jpg': 'image/jpeg',
382
- '.jpeg': 'image/jpeg',
383
- '.gif': 'image/gif',
384
- '.svg': 'image/svg+xml',
385
- '.ico': 'image/x-icon',
386
- '.webp': 'image/webp',
387
- '.woff': 'font/woff',
388
- '.woff2': 'font/woff2',
389
- '.ttf': 'font/ttf',
390
- '.eot': 'application/vnd.ms-fontobject',
391
- };
392
- return mimeTypes[ext] || 'application/octet-stream';
393
- }
394
369
  /**
395
370
  * Injects live reload script into HTML responses
396
371
  */
@@ -461,9 +436,9 @@ export async function createDevServer(options = {}) {
461
436
  statusCode: 500,
462
437
  };
463
438
  }
464
- const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
465
- // Security: Prevent path traversal attacks
466
- if (!isPathWithinDirectory(outDir, originalFilePath)) {
439
+ // Security: the file the request names, or null when it is outside the output directory
440
+ const requestFile = resolveRequestFile(outDir, requestPath);
441
+ if (requestFile === null) {
467
442
  return {
468
443
  content: '403 - Forbidden',
469
444
  mimeType: 'text/plain',
@@ -471,7 +446,7 @@ export async function createDevServer(options = {}) {
471
446
  };
472
447
  }
473
448
  // Use the shared pretty URL resolver
474
- const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
449
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, requestFile);
475
450
  if (!found || !filePath) {
476
451
  // If we have a build error and this is an HTML request, show error overlay instead of 404
477
452
  const errorToShow = lastBuildError;
@@ -508,6 +483,16 @@ export async function createDevServer(options = {}) {
508
483
  statusCode: 500,
509
484
  };
510
485
  }
486
+ // Serve the site's 404 page, as static hosts do, with live reload so it updates on edits
487
+ const notFoundPage = await resolveNotFoundPage(outDir);
488
+ if (notFoundPage) {
489
+ const html = await readFile(notFoundPage, 'utf-8');
490
+ return {
491
+ content: injectLiveReloadScript(html),
492
+ mimeType: 'text/html',
493
+ statusCode: 404,
494
+ };
495
+ }
511
496
  return {
512
497
  content: requestPath.endsWith('/')
513
498
  ? '404 - Directory listing not available'
@@ -601,12 +586,17 @@ export async function createDevServer(options = {}) {
601
586
  await performInitialBuild(configPath, logger, setLastBuildError);
602
587
  // Create HTTP server
603
588
  httpServer = createServer(async (req, res) => {
604
- const requestPath = req.url || '/';
589
+ const requestPath = parseRequestPath(req.url);
605
590
  const requestStart = Date.now();
606
591
  // Handle WebSocket upgrade path
607
592
  if (requestPath === '/__ws') {
608
593
  return; // Let WebSocket server handle this
609
594
  }
595
+ if (requestPath === null) {
596
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
597
+ res.end('400 - Bad Request');
598
+ return;
599
+ }
610
600
  // Only log page requests, not static assets (files with extensions)
611
601
  const hasFileExtension = requestPath.includes('.') && !requestPath.endsWith('.html');
612
602
  if (!hasFileExtension) {
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,mBAAmB,CAAC;AAa7D,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAmKxB"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,mBAAmB,CAAC;AAgB7D,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAwJxB"}
@@ -1,8 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
- import { join, extname } from 'node:path';
3
2
  import { readFile } from 'node:fs/promises';
4
3
  import { loadConfig } from '../config/loader.js';
5
- import { resolveDevPaths, resolvePrettyUrl, createFallbackLogger, mergeServerOptions, isPathWithinDirectory, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
4
+ import { resolveDevPaths, resolvePrettyUrl, resolveRequestFile, resolveNotFoundPage, getMimeType, parseRequestPath, createFallbackLogger, mergeServerOptions, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
6
5
  import { DEFAULT_PREVIEW_PORT, DEFAULT_DEV_HOST } from '../constants.js';
7
6
  /**
8
7
  * Loads and validates configuration for the preview server.
@@ -41,37 +40,13 @@ export async function createPreviewServer(options = {}) {
41
40
  });
42
41
  const url = `http://${host}:${port}`;
43
42
  let httpServer = null;
44
- /**
45
- * Gets MIME type for a file based on its extension
46
- */
47
- function getMimeType(filePath) {
48
- const ext = extname(filePath).toLowerCase();
49
- const mimeTypes = {
50
- '.html': 'text/html',
51
- '.js': 'application/javascript',
52
- '.css': 'text/css',
53
- '.json': 'application/json',
54
- '.png': 'image/png',
55
- '.jpg': 'image/jpeg',
56
- '.jpeg': 'image/jpeg',
57
- '.gif': 'image/gif',
58
- '.svg': 'image/svg+xml',
59
- '.ico': 'image/x-icon',
60
- '.webp': 'image/webp',
61
- '.woff': 'font/woff',
62
- '.woff2': 'font/woff2',
63
- '.ttf': 'font/ttf',
64
- '.eot': 'application/vnd.ms-fontobject',
65
- };
66
- return mimeTypes[ext] || 'application/octet-stream';
67
- }
68
43
  /**
69
44
  * Serves files from the dist directory
70
45
  */
71
46
  async function serveFile(requestPath) {
72
- const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
73
- // Security: Prevent path traversal attacks
74
- if (!isPathWithinDirectory(outDir, originalFilePath)) {
47
+ // Security: the file the request names, or null when it is outside the output directory
48
+ const requestFile = resolveRequestFile(outDir, requestPath);
49
+ if (requestFile === null) {
75
50
  return {
76
51
  content: '403 - Forbidden',
77
52
  mimeType: 'text/plain',
@@ -79,8 +54,17 @@ export async function createPreviewServer(options = {}) {
79
54
  };
80
55
  }
81
56
  // Use the shared pretty URL resolver
82
- const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
57
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, requestFile);
83
58
  if (!found || !filePath) {
59
+ // Serve the site's 404 page, as static hosts do
60
+ const notFoundPage = await resolveNotFoundPage(outDir);
61
+ if (notFoundPage) {
62
+ return {
63
+ content: await readFile(notFoundPage),
64
+ mimeType: 'text/html',
65
+ statusCode: 404,
66
+ };
67
+ }
84
68
  return {
85
69
  content: requestPath.endsWith('/')
86
70
  ? '404 - Directory listing not available'
@@ -114,8 +98,13 @@ export async function createPreviewServer(options = {}) {
114
98
  await assertPortAvailable(port, host, 'preview.port');
115
99
  // Create HTTP server
116
100
  httpServer = createServer(async (req, res) => {
117
- const requestPath = req.url || '/';
118
- logger.processing?.(`${req.method} ${requestPath}`);
101
+ logger.processing?.(`${req.method} ${req.url || '/'}`);
102
+ const requestPath = parseRequestPath(req.url);
103
+ if (requestPath === null) {
104
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
105
+ res.end('400 - Bad Request');
106
+ return;
107
+ }
119
108
  try {
120
109
  const { content, mimeType, statusCode } = await serveFile(requestPath);
121
110
  res.writeHead(statusCode, {
@@ -23,6 +23,15 @@ export interface RenderPageExtras {
23
23
  /** Image state of the page; without it `stati.image()` emits a plain `<img>` */
24
24
  images?: PageImages | undefined;
25
25
  }
26
+ /**
27
+ * Discovers partials in the hierarchy for a given page path.
28
+ * Scans all parent directories for folders starting with underscore.
29
+ *
30
+ * @param pagePath - The path to the page (relative to srcDir), with `/` or `\` separators
31
+ * @param config - Stati configuration
32
+ * @returns Object mapping partial names to their file paths
33
+ */
34
+ export declare function discoverPartials(pagePath: string, config: StatiConfig): Promise<Record<string, string>>;
26
35
  /**
27
36
  * Clear the cached template engine. Call this when templates change in dev mode
28
37
  * so the next build recompiles them, or when shutting down a dev session.
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAuB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AAuHD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8RvB"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,OAAO,EAEP,QAAQ,EACR,MAAM,EACP,MAAM,mBAAmB,CAAC;AAuB3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,eAAe,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACjC;AA2CD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoDjC;AAcD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CA2B7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,EACtB,MAAM,CAAC,EAAE,OAAO,mBAAmB,EAAE,WAAW,EAChD,GAAG,CAAC,EAAE,QAAQ,EAAE,EAChB,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8RvB"}
@@ -1,5 +1,5 @@
1
1
  import { Eta } from 'eta';
2
- import { join, dirname, relative, basename, posix } from 'node:path';
2
+ import { join, relative, basename, posix } from 'node:path';
3
3
  import glob from 'fast-glob';
4
4
  import { createFallbackLogger } from './utils/index.js';
5
5
  import { TEMPLATE_EXTENSION } from '../constants.js';
@@ -50,15 +50,16 @@ function createNavView(helpers, reads) {
50
50
  * Discovers partials in the hierarchy for a given page path.
51
51
  * Scans all parent directories for folders starting with underscore.
52
52
  *
53
- * @param pagePath - The path to the page (relative to srcDir)
53
+ * @param pagePath - The path to the page (relative to srcDir), with `/` or `\` separators
54
54
  * @param config - Stati configuration
55
55
  * @returns Object mapping partial names to their file paths
56
56
  */
57
- async function discoverPartials(pagePath, config) {
57
+ export async function discoverPartials(pagePath, config) {
58
58
  const srcDir = resolveSrcDir(config);
59
59
  const partials = {};
60
- // Get the directory of the current page
61
- const pageDir = dirname(pagePath);
60
+ // Get the directory of the current page. Windows paths use backslashes, so convert them
61
+ // first, or the folders between the root and the page's own folder are skipped.
62
+ const pageDir = posix.dirname(pagePath.replace(/\\/g, '/'));
62
63
  const pathSegments = pageDir === '.' ? [] : pageDir.split('/');
63
64
  // Scan from root to current directory (least specific first)
64
65
  // This allows more specific partials to override less specific ones
@@ -55,7 +55,7 @@ export interface CompiledBundleInfo {
55
55
  * 3. If page matches any `exclude` pattern: bundle is excluded (exclude takes precedence)
56
56
  * 4. Bundle order from config is preserved
57
57
  *
58
- * @param pageOutputPath - The page's output URL path (e.g., '/docs/api/hooks.html')
58
+ * @param pageUrl - The page URL (e.g., '/docs/api/hooks'), not its output file
59
59
  * @param bundles - Array of bundle configurations to filter
60
60
  * @returns Array of bundles that match this page, in config order
61
61
  *
@@ -67,22 +67,22 @@ export interface CompiledBundleInfo {
67
67
  * { entryPoint: 'playground.ts', bundleName: 'playground', include: ['/examples/**'], exclude: ['/examples/simple/**'] }
68
68
  * ];
69
69
  *
70
- * matchBundlesForPage('/docs/api/hooks.html', bundles);
70
+ * matchBundlesForPage('/docs/api/hooks', bundles);
71
71
  * // Returns: [{ entryPoint: 'core.ts', ... }, { entryPoint: 'docs.ts', ... }]
72
72
  *
73
- * matchBundlesForPage('/examples/advanced/demo.html', bundles);
73
+ * matchBundlesForPage('/examples/advanced/demo', bundles);
74
74
  * // Returns: [{ entryPoint: 'core.ts', ... }, { entryPoint: 'playground.ts', ... }]
75
75
  *
76
- * matchBundlesForPage('/examples/simple/basic.html', bundles);
76
+ * matchBundlesForPage('/examples/simple/basic', bundles);
77
77
  * // Returns: [{ entryPoint: 'core.ts', ... }] - playground excluded
78
78
  * ```
79
79
  */
80
- export declare function matchBundlesForPage(pageOutputPath: string, bundles: BundleConfig[]): BundleConfig[];
80
+ export declare function matchBundlesForPage(pageUrl: string, bundles: BundleConfig[]): BundleConfig[];
81
81
  /**
82
82
  * Gets bundle paths for a page from compiled bundle info.
83
83
  * Filters compiled bundles based on page matching and returns only the paths.
84
84
  *
85
- * @param pageOutputPath - The page's output URL path (e.g., '/docs/api/hooks.html')
85
+ * @param pageUrl - The page URL (e.g., '/docs/api/hooks'), not its output file
86
86
  * @param compiledBundles - Array of compiled bundle info
87
87
  * @returns Array of bundle paths matched for this page, in config order
88
88
  *
@@ -93,9 +93,9 @@ export declare function matchBundlesForPage(pageOutputPath: string, bundles: Bun
93
93
  * { config: { entryPoint: 'docs.ts', bundleName: 'docs', include: ['/docs/**'] }, filename: 'docs-def.js', path: '/_assets/docs-def.js' }
94
94
  * ];
95
95
  *
96
- * getBundlePathsForPage('/docs/api/hooks.html', compiled);
96
+ * getBundlePathsForPage('/docs/api/hooks', compiled);
97
97
  * // Returns: ['/_assets/core-abc.js', '/_assets/docs-def.js']
98
98
  * ```
99
99
  */
100
- export declare function getBundlePathsForPage(pageOutputPath: string, compiledBundles: CompiledBundleInfo[]): string[];
100
+ export declare function getBundlePathsForPage(pageUrl: string, compiledBundles: CompiledBundleInfo[]): string[];
101
101
  //# sourceMappingURL=bundle-matching.utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-matching.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/bundle-matching.utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,UAAU,EAAE,MAAM,EAAE;CAOjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,IAAI,CAqBvE;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,MAAM,EAAE,YAAY,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,YAAY,EAAE,GACtB,YAAY,EAAE,CAyBhB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,kBAAkB,EAAE,GACpC,MAAM,EAAE,CAaV"}
1
+ {"version":3,"file":"bundle-matching.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/bundle-matching.utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,UAAU,EAAE,MAAM,EAAE;CAOjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,IAAI,CAqBvE;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,MAAM,EAAE,YAAY,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAyB5F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,kBAAkB,EAAE,GACpC,MAAM,EAAE,CAaV"}
@@ -64,7 +64,7 @@ export function validateUniqueBundleNames(bundles) {
64
64
  * 3. If page matches any `exclude` pattern: bundle is excluded (exclude takes precedence)
65
65
  * 4. Bundle order from config is preserved
66
66
  *
67
- * @param pageOutputPath - The page's output URL path (e.g., '/docs/api/hooks.html')
67
+ * @param pageUrl - The page URL (e.g., '/docs/api/hooks'), not its output file
68
68
  * @param bundles - Array of bundle configurations to filter
69
69
  * @returns Array of bundles that match this page, in config order
70
70
  *
@@ -76,22 +76,22 @@ export function validateUniqueBundleNames(bundles) {
76
76
  * { entryPoint: 'playground.ts', bundleName: 'playground', include: ['/examples/**'], exclude: ['/examples/simple/**'] }
77
77
  * ];
78
78
  *
79
- * matchBundlesForPage('/docs/api/hooks.html', bundles);
79
+ * matchBundlesForPage('/docs/api/hooks', bundles);
80
80
  * // Returns: [{ entryPoint: 'core.ts', ... }, { entryPoint: 'docs.ts', ... }]
81
81
  *
82
- * matchBundlesForPage('/examples/advanced/demo.html', bundles);
82
+ * matchBundlesForPage('/examples/advanced/demo', bundles);
83
83
  * // Returns: [{ entryPoint: 'core.ts', ... }, { entryPoint: 'playground.ts', ... }]
84
84
  *
85
- * matchBundlesForPage('/examples/simple/basic.html', bundles);
85
+ * matchBundlesForPage('/examples/simple/basic', bundles);
86
86
  * // Returns: [{ entryPoint: 'core.ts', ... }] - playground excluded
87
87
  * ```
88
88
  */
89
- export function matchBundlesForPage(pageOutputPath, bundles) {
89
+ export function matchBundlesForPage(pageUrl, bundles) {
90
90
  if (!bundles || bundles.length === 0) {
91
91
  return [];
92
92
  }
93
93
  // Normalize path to use forward slashes
94
- const normalizedPath = pageOutputPath.replace(/\\/g, '/');
94
+ const normalizedPath = pageUrl.replace(/\\/g, '/');
95
95
  return bundles.filter((bundle) => {
96
96
  // Check exclude patterns first (they take precedence)
97
97
  if (bundle.exclude && bundle.exclude.length > 0) {
@@ -112,7 +112,7 @@ export function matchBundlesForPage(pageOutputPath, bundles) {
112
112
  * Gets bundle paths for a page from compiled bundle info.
113
113
  * Filters compiled bundles based on page matching and returns only the paths.
114
114
  *
115
- * @param pageOutputPath - The page's output URL path (e.g., '/docs/api/hooks.html')
115
+ * @param pageUrl - The page URL (e.g., '/docs/api/hooks'), not its output file
116
116
  * @param compiledBundles - Array of compiled bundle info
117
117
  * @returns Array of bundle paths matched for this page, in config order
118
118
  *
@@ -123,15 +123,15 @@ export function matchBundlesForPage(pageOutputPath, bundles) {
123
123
  * { config: { entryPoint: 'docs.ts', bundleName: 'docs', include: ['/docs/**'] }, filename: 'docs-def.js', path: '/_assets/docs-def.js' }
124
124
  * ];
125
125
  *
126
- * getBundlePathsForPage('/docs/api/hooks.html', compiled);
126
+ * getBundlePathsForPage('/docs/api/hooks', compiled);
127
127
  * // Returns: ['/_assets/core-abc.js', '/_assets/docs-def.js']
128
128
  * ```
129
129
  */
130
- export function getBundlePathsForPage(pageOutputPath, compiledBundles) {
130
+ export function getBundlePathsForPage(pageUrl, compiledBundles) {
131
131
  if (!compiledBundles || compiledBundles.length === 0) {
132
132
  return [];
133
133
  }
134
- const matchedConfigs = matchBundlesForPage(pageOutputPath, compiledBundles.map((b) => b.config));
134
+ const matchedConfigs = matchBundlesForPage(pageUrl, compiledBundles.map((b) => b.config));
135
135
  // Map matched configs back to their paths, preserving order
136
136
  const matchedNames = new Set(matchedConfigs.map((c) => c.bundleName));
137
137
  return compiledBundles.filter((b) => matchedNames.has(b.config.bundleName)).map((b) => b.path);
@@ -15,7 +15,7 @@ export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials
15
15
  export type { CallablePartial } from './callable-partials.utils.js';
16
16
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.utils.js';
17
17
  export { createNavigationHelpers } from './navigation-helpers.utils.js';
18
- export { resolvePrettyUrl, mergeServerOptions, assertPortAvailable, toPortUnavailableError, PortUnavailableError, } from './server.utils.js';
18
+ export { getMimeType, parseRequestPath, resolveNotFoundPage, resolveRequestFile, resolvePrettyUrl, mergeServerOptions, assertPortAvailable, toPortUnavailableError, PortUnavailableError, } from './server.utils.js';
19
19
  export type { PrettyUrlResult, MergedServerOptions, MergeServerOptionsParams, PortUnavailableCode, } from './server.utils.js';
20
20
  export { createErrorOverlay, parseErrorDetails } from './error-overlay.utils.js';
21
21
  export type { ErrorDetails } from './error-overlay.utils.js';
@@ -24,8 +24,8 @@ export { globToRegex, matchesGlob } from './glob-patterns.utils.js';
24
24
  export { matchBundlesForPage, getBundlePathsForPage, validateUniqueBundleNames, DuplicateBundleNameError, } from './bundle-matching.utils.js';
25
25
  export type { CompiledBundleInfo } from './bundle-matching.utils.js';
26
26
  export { createFallbackLogger } from './logger.utils.js';
27
- export { compileTypeScript, createTypeScriptWatcher, compileStatiConfig, cleanupCompiledConfig, autoInjectBundles, formatBytes, detectExistingBundles, } from './typescript.utils.js';
28
- export type { CompileOptions, WatchOptions } from './typescript.utils.js';
27
+ export { compileTypeScript, createTypeScriptWatcher, compileStatiConfig, cleanupCompiledConfig, autoInjectBundles, formatBytes, detectExistingBundles, removeStaleBundleFiles, } from './typescript.utils.js';
28
+ export type { CompileOptions, WatchOptions, RemoveStaleBundlesOptions, } from './typescript.utils.js';
29
29
  export { findHeadClosePosition, injectBeforeHeadClose } from './html.utils.js';
30
30
  export { getFrontMatterDate, PUBLISHED_DATE_FIELDS } from './date.utils.js';
31
31
  export { isEnabledEnvFlag } from './env.utils.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"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"}