@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.
Files changed (74) hide show
  1. package/README.md +6 -1
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +192 -71
  4. package/dist/core/collections.d.ts +139 -0
  5. package/dist/core/collections.d.ts.map +1 -0
  6. package/dist/core/collections.js +342 -0
  7. package/dist/core/content.d.ts.map +1 -1
  8. package/dist/core/content.js +2 -1
  9. package/dist/core/dev.d.ts.map +1 -1
  10. package/dist/core/dev.js +32 -93
  11. package/dist/core/expand.d.ts +86 -0
  12. package/dist/core/expand.d.ts.map +1 -0
  13. package/dist/core/expand.js +484 -0
  14. package/dist/core/isg/builder.d.ts +15 -2
  15. package/dist/core/isg/builder.d.ts.map +1 -1
  16. package/dist/core/isg/builder.js +44 -6
  17. package/dist/core/isg/hash.d.ts +4 -2
  18. package/dist/core/isg/hash.d.ts.map +1 -1
  19. package/dist/core/isg/hash.js +16 -2
  20. package/dist/core/isg/index.d.ts +2 -1
  21. package/dist/core/isg/index.d.ts.map +1 -1
  22. package/dist/core/isg/index.js +3 -1
  23. package/dist/core/isg/manifest.js +6 -0
  24. package/dist/core/isg/prune.d.ts +32 -0
  25. package/dist/core/isg/prune.d.ts.map +1 -0
  26. package/dist/core/isg/prune.js +113 -0
  27. package/dist/core/navigation.d.ts +4 -2
  28. package/dist/core/navigation.d.ts.map +1 -1
  29. package/dist/core/navigation.js +17 -2
  30. package/dist/core/preview.d.ts.map +1 -1
  31. package/dist/core/preview.js +21 -32
  32. package/dist/core/templates.d.ts +3 -0
  33. package/dist/core/templates.d.ts.map +1 -1
  34. package/dist/core/templates.js +67 -127
  35. package/dist/core/utils/bundle-matching.utils.d.ts +8 -8
  36. package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -1
  37. package/dist/core/utils/bundle-matching.utils.js +10 -10
  38. package/dist/core/utils/fs.utils.d.ts +8 -0
  39. package/dist/core/utils/fs.utils.d.ts.map +1 -1
  40. package/dist/core/utils/fs.utils.js +13 -1
  41. package/dist/core/utils/index.d.ts +6 -6
  42. package/dist/core/utils/index.d.ts.map +1 -1
  43. package/dist/core/utils/index.js +5 -5
  44. package/dist/core/utils/paths.utils.d.ts +15 -0
  45. package/dist/core/utils/paths.utils.d.ts.map +1 -1
  46. package/dist/core/utils/paths.utils.js +20 -0
  47. package/dist/core/utils/server.utils.d.ts +38 -3
  48. package/dist/core/utils/server.utils.d.ts.map +1 -1
  49. package/dist/core/utils/server.utils.js +132 -44
  50. package/dist/core/utils/template-discovery.utils.d.ts +25 -1
  51. package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
  52. package/dist/core/utils/template-discovery.utils.js +61 -11
  53. package/dist/core/utils/typescript.utils.d.ts +27 -0
  54. package/dist/core/utils/typescript.utils.d.ts.map +1 -1
  55. package/dist/core/utils/typescript.utils.js +34 -1
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/rss/generator.d.ts.map +1 -1
  59. package/dist/rss/generator.js +5 -2
  60. package/dist/search/generator.d.ts +27 -7
  61. package/dist/search/generator.d.ts.map +1 -1
  62. package/dist/search/generator.js +55 -12
  63. package/dist/search/index.d.ts +1 -1
  64. package/dist/search/index.d.ts.map +1 -1
  65. package/dist/search/index.js +1 -1
  66. package/dist/types/config.d.ts +61 -4
  67. package/dist/types/config.d.ts.map +1 -1
  68. package/dist/types/content.d.ts +129 -1
  69. package/dist/types/content.d.ts.map +1 -1
  70. package/dist/types/index.d.ts +2 -2
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/dist/types/isg.d.ts +12 -0
  73. package/dist/types/isg.d.ts.map +1 -1
  74. package/package.json +1 -1
@@ -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);
@@ -24,6 +24,14 @@ export declare function remove(path: string): Promise<void>;
24
24
  * Gets file stats with consistent error handling.
25
25
  */
26
26
  export declare function stat(filePath: string): Promise<fse.Stats>;
27
+ /**
28
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
29
+ */
30
+ export declare function lstat(filePath: string): Promise<fse.Stats>;
31
+ /**
32
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
33
+ */
34
+ export declare function realpath(filePath: string): Promise<string>;
27
35
  /**
28
36
  * Reads directory contents with consistent error handling.
29
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAmDvD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,CAAC,CAAA;CAAE,GAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAcnD;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAU7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAchE"}
1
+ {"version":3,"file":"fs.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/fs.utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAqDvD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,OAAO,GAAG,MAAgB,GACnC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnE;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAEhE;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEhE;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,OAAO,GAAG,KAAK,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,CAAC,CAAA;CAAE,GAC9B,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAcnD;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE7E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAU7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAchE"}
@@ -1,7 +1,7 @@
1
1
  import fse from 'fs-extra';
2
2
  import { readFileSync, statSync, readdirSync } from 'node:fs';
3
3
  import { join, relative } from 'node:path';
4
- const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
4
+ const { readFile: fseReadFile, writeFile: fseWriteFile, pathExists: fsePathExists, ensureDir: fseEnsureDir, remove: fseRemove, stat: fseStat, lstat: fseLstat, realpath: fseRealpath, readdir: fseReaddir, copyFile: fseCopyFile, } = fse;
5
5
  /**
6
6
  * Wraps fs-extra operations with consistent error handling.
7
7
  */
@@ -65,6 +65,18 @@ export async function remove(path) {
65
65
  export async function stat(filePath) {
66
66
  return wrapFsOperation('get stats for', fseStat(filePath), filePath);
67
67
  }
68
+ /**
69
+ * Gets file stats without following a symbolic link at the path, with consistent error handling.
70
+ */
71
+ export async function lstat(filePath) {
72
+ return wrapFsOperation('get stats for', fseLstat(filePath), filePath);
73
+ }
74
+ /**
75
+ * Resolves a path to its real location, following symbolic links, with consistent error handling.
76
+ */
77
+ export async function realpath(filePath) {
78
+ return wrapFsOperation('resolve', fseRealpath(filePath), filePath);
79
+ }
68
80
  /**
69
81
  * Reads directory contents with consistent error handling.
70
82
  */
@@ -2,9 +2,9 @@
2
2
  * Core utilities index
3
3
  * @module core/utils
4
4
  */
5
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
7
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
5
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
7
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
9
9
  export { slugify } from './slugify.utils.js';
10
10
  export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.utils.js';
@@ -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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"}
@@ -3,11 +3,11 @@
3
3
  * @module core/utils
4
4
  */
5
5
  // File system utilities
6
- export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
6
+ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, lstat, realpath, readFileBuffer, readFileBufferSync, statSyncOrNull, listFilesRecursiveSync, } from './fs.utils.js';
7
7
  // Path resolution utilities
8
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
8
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, getPageOutputFile, } from './paths.utils.js';
9
9
  // Template discovery utilities
10
- export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
10
+ export { discoverLayout, isCollectionIndexPage, isVirtualPage, getFirstPaginationPage, getCollectionPathForPage, } from './template-discovery.utils.js';
11
11
  // Template utilities
12
12
  export { propValue } from './template.utils.js';
13
13
  // Slugify utilities
@@ -27,7 +27,7 @@ export { TemplateError, parseEtaError, createTemplateError } from './template-er
27
27
  // Navigation helper utilities
28
28
  export { createNavigationHelpers } from './navigation-helpers.utils.js';
29
29
  // Server utilities
30
- export { resolvePrettyUrl, mergeServerOptions, assertPortAvailable, toPortUnavailableError, PortUnavailableError, } from './server.utils.js';
30
+ export { getMimeType, parseRequestPath, resolveNotFoundPage, resolveRequestFile, resolvePrettyUrl, mergeServerOptions, assertPortAvailable, toPortUnavailableError, PortUnavailableError, } from './server.utils.js';
31
31
  // Error overlay utilities
32
32
  export { createErrorOverlay, parseErrorDetails } from './error-overlay.utils.js';
33
33
  // Version utilities
@@ -39,7 +39,7 @@ export { matchBundlesForPage, getBundlePathsForPage, validateUniqueBundleNames,
39
39
  // Logger utilities
40
40
  export { createFallbackLogger } from './logger.utils.js';
41
41
  // TypeScript compilation utilities
42
- export { compileTypeScript, createTypeScriptWatcher, compileStatiConfig, cleanupCompiledConfig, autoInjectBundles, formatBytes, detectExistingBundles, } from './typescript.utils.js';
42
+ export { compileTypeScript, createTypeScriptWatcher, compileStatiConfig, cleanupCompiledConfig, autoInjectBundles, formatBytes, detectExistingBundles, removeStaleBundleFiles, } from './typescript.utils.js';
43
43
  // HTML manipulation utilities
44
44
  export { findHeadClosePosition, injectBeforeHeadClose } from './html.utils.js';
45
45
  // Date utilities
@@ -131,4 +131,19 @@ export declare function normalizePathForComparison(filePath: string, basePath?:
131
131
  * ```
132
132
  */
133
133
  export declare function isPathWithinDirectory(baseDir: string, targetPath: string): boolean;
134
+ /**
135
+ * The file a page URL is written to, relative to the output directory. The build joins it to
136
+ * the output directory, which also normalizes separators and `.` segments.
137
+ *
138
+ * @param url - The page URL
139
+ * @returns The output file
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * getPageOutputFile('/'); // 'index.html'
144
+ * getPageOutputFile('/blog/'); // '/blog/index.html'
145
+ * getPageOutputFile('/blog/post'); // '/blog/post.html'
146
+ * ```
147
+ */
148
+ export declare function getPageOutputFile(url: string): string;
134
149
  //# sourceMappingURL=paths.utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF"}
1
+ {"version":3,"file":"paths.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/paths.utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAQxD;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW;;;;EAMlD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA6BtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKrD"}
@@ -183,3 +183,23 @@ export function isPathWithinDirectory(baseDir, targetPath) {
183
183
  // We also allow exact match (resolvedTarget === resolvedBase) for the root
184
184
  return resolvedTarget === resolvedBase || resolvedTarget.startsWith(normalizedBase);
185
185
  }
186
+ /**
187
+ * The file a page URL is written to, relative to the output directory. The build joins it to
188
+ * the output directory, which also normalizes separators and `.` segments.
189
+ *
190
+ * @param url - The page URL
191
+ * @returns The output file
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * getPageOutputFile('/'); // 'index.html'
196
+ * getPageOutputFile('/blog/'); // '/blog/index.html'
197
+ * getPageOutputFile('/blog/post'); // '/blog/post.html'
198
+ * ```
199
+ */
200
+ export function getPageOutputFile(url) {
201
+ if (url === '/') {
202
+ return 'index.html';
203
+ }
204
+ return url.endsWith('/') ? `${url}index.html` : `${url}.html`;
205
+ }
@@ -1,3 +1,26 @@
1
+ /**
2
+ * Gets the content type for a file from its extension.
3
+ *
4
+ * @param filePath - Path or file name of the file being served
5
+ * @returns The content type, or `application/octet-stream` for unknown extensions
6
+ */
7
+ export declare function getMimeType(filePath: string): string;
8
+ /**
9
+ * Turns a request URL into the path of the file it asks for: the query string is dropped and
10
+ * percent-escapes are decoded, so `/about?ref=x` asks for `/about` and `/my%20file.pdf` for
11
+ * `/my file.pdf`.
12
+ *
13
+ * @param url - The request URL as received (`req.url`)
14
+ * @returns The decoded path, or null when the URL is malformed (a bad escape or a NUL byte)
15
+ */
16
+ export declare function parseRequestPath(url: string | undefined): string | null;
17
+ /**
18
+ * Finds the site's custom not-found page, the one static hosts serve for a missing URL.
19
+ *
20
+ * @param outDir - The output directory being served
21
+ * @returns The path of `404.html` (or `404/index.html` for a `/404/` permalink), or null
22
+ */
23
+ export declare function resolveNotFoundPage(outDir: string): Promise<string | null>;
1
24
  /**
2
25
  * Error codes for which a port is considered unavailable
3
26
  */
@@ -42,6 +65,17 @@ export interface PrettyUrlResult {
42
65
  /** Whether the path was found */
43
66
  found: boolean;
44
67
  }
68
+ /**
69
+ * Resolves a request path to the file it names inside the served directory. This is where a
70
+ * request path becomes a file system path: every file the servers read is this path or
71
+ * derived from it, so no request reaches outside the directory, whether through `..`
72
+ * segments (raw or percent-encoded), backslashes or an absolute path.
73
+ *
74
+ * @param outDir - The directory being served
75
+ * @param requestPath - The decoded request path, from {@link parseRequestPath}
76
+ * @returns The absolute path, or null when it points outside the directory
77
+ */
78
+ export declare function resolveRequestFile(outDir: string, requestPath: string): string | null;
45
79
  /**
46
80
  * Attempts to resolve pretty URL paths by trying various fallback strategies.
47
81
  * This handles common patterns like:
@@ -50,11 +84,12 @@ export interface PrettyUrlResult {
50
84
  * - /path -> /path.html (when original path is not found and has no extension)
51
85
  *
52
86
  * @param outDir The output directory to serve files from
53
- * @param requestPath The requested URL path
54
- * @param originalFilePath The original file path that was attempted
87
+ * @param requestPath The requested URL path, which decides the fallbacks
88
+ * @param requestFile The file the request names, from {@link resolveRequestFile}. Every
89
+ * candidate is derived from it, so none is outside the output directory
55
90
  * @returns Promise resolving to the file path to serve or null if not found
56
91
  */
57
- export declare function resolvePrettyUrl(outDir: string, requestPath: string, originalFilePath: string): Promise<PrettyUrlResult>;
92
+ export declare function resolvePrettyUrl(outDir: string, requestPath: string, requestFile: string): Promise<PrettyUrlResult>;
58
93
  /**
59
94
  * Options for merging server configuration
60
95
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/server.utils.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAE1D;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;gBAEvB,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,SAAS,CAAC,EAAE,MAAM;CAatF;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAMT;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjG;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,eAAe,CAAC,CA2D1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,sCAAsC;IACtC,OAAO,EAAE,CAAC,CAAC;IACX,4CAA4C;IAC5C,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACrE,4EAA4E;IAC5E,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,OAAO,CAAC;KACf,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3F,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAClC,mBAAmB,CAQrB"}
1
+ {"version":3,"file":"server.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/server.utils.ts"],"names":[],"mappings":"AAsCA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAQvE;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWhF;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAE1D;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;gBAEvB,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,SAAS,CAAC,EAAE,MAAM;CAatF;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAMT;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjG;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWrF;AAkBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CA8B1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,sCAAsC;IACtC,OAAO,EAAE,CAAC,CAAC;IACX,4CAA4C;IAC5C,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;IACrE,4EAA4E;IAC5E,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,OAAO,CAAC;KACf,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3F,MAAM,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAClC,mBAAmB,CAQrB"}
@@ -1,6 +1,85 @@
1
- import { join } from 'node:path';
1
+ import { extname, join, resolve, sep } from 'node:path';
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { createServer } from 'node:net';
4
+ /**
5
+ * Content types the dev and preview servers send, by file extension. Covers the files Stati
6
+ * generates (pages, bundles, search index, sitemap, robots.txt, feeds, responsive images) and
7
+ * common static assets; anything else is sent as `application/octet-stream`.
8
+ */
9
+ const MIME_TYPES = {
10
+ '.html': 'text/html',
11
+ '.js': 'application/javascript',
12
+ '.mjs': 'application/javascript',
13
+ '.css': 'text/css',
14
+ '.json': 'application/json',
15
+ '.map': 'application/json',
16
+ '.webmanifest': 'application/manifest+json',
17
+ '.xml': 'application/xml',
18
+ '.txt': 'text/plain',
19
+ '.png': 'image/png',
20
+ '.jpg': 'image/jpeg',
21
+ '.jpeg': 'image/jpeg',
22
+ '.gif': 'image/gif',
23
+ '.svg': 'image/svg+xml',
24
+ '.ico': 'image/x-icon',
25
+ '.webp': 'image/webp',
26
+ '.avif': 'image/avif',
27
+ '.woff': 'font/woff',
28
+ '.woff2': 'font/woff2',
29
+ '.ttf': 'font/ttf',
30
+ '.otf': 'font/otf',
31
+ '.eot': 'application/vnd.ms-fontobject',
32
+ '.pdf': 'application/pdf',
33
+ '.mp4': 'video/mp4',
34
+ '.webm': 'video/webm',
35
+ '.wasm': 'application/wasm',
36
+ };
37
+ /**
38
+ * Gets the content type for a file from its extension.
39
+ *
40
+ * @param filePath - Path or file name of the file being served
41
+ * @returns The content type, or `application/octet-stream` for unknown extensions
42
+ */
43
+ export function getMimeType(filePath) {
44
+ return MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
45
+ }
46
+ /**
47
+ * Turns a request URL into the path of the file it asks for: the query string is dropped and
48
+ * percent-escapes are decoded, so `/about?ref=x` asks for `/about` and `/my%20file.pdf` for
49
+ * `/my file.pdf`.
50
+ *
51
+ * @param url - The request URL as received (`req.url`)
52
+ * @returns The decoded path, or null when the URL is malformed (a bad escape or a NUL byte)
53
+ */
54
+ export function parseRequestPath(url) {
55
+ const rawPath = (url || '/').split(/[?#]/, 1)[0] || '/';
56
+ try {
57
+ const path = decodeURIComponent(rawPath);
58
+ return path.includes('\0') ? null : path;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Finds the site's custom not-found page, the one static hosts serve for a missing URL.
66
+ *
67
+ * @param outDir - The output directory being served
68
+ * @returns The path of `404.html` (or `404/index.html` for a `/404/` permalink), or null
69
+ */
70
+ export async function resolveNotFoundPage(outDir) {
71
+ for (const candidate of [join(outDir, '404.html'), join(outDir, '404', 'index.html')]) {
72
+ try {
73
+ if ((await stat(candidate)).isFile()) {
74
+ return candidate;
75
+ }
76
+ }
77
+ catch {
78
+ // Not there; try the next one
79
+ }
80
+ }
81
+ return null;
82
+ }
4
83
  /**
5
84
  * Thrown when a server cannot bind to its configured port.
6
85
  * The message is user-facing and explains how to pick a different port.
@@ -59,6 +138,44 @@ export function assertPortAvailable(port, host, configKey) {
59
138
  probe.listen(port, host);
60
139
  });
61
140
  }
141
+ /**
142
+ * Resolves a request path to the file it names inside the served directory. This is where a
143
+ * request path becomes a file system path: every file the servers read is this path or
144
+ * derived from it, so no request reaches outside the directory, whether through `..`
145
+ * segments (raw or percent-encoded), backslashes or an absolute path.
146
+ *
147
+ * @param outDir - The directory being served
148
+ * @param requestPath - The decoded request path, from {@link parseRequestPath}
149
+ * @returns The absolute path, or null when it points outside the directory
150
+ */
151
+ export function resolveRequestFile(outDir, requestPath) {
152
+ const root = resolve(outDir);
153
+ const filePath = resolve(root, requestPath.replace(/^[/\\]+/, ''));
154
+ if (filePath === root) {
155
+ return root;
156
+ }
157
+ const rootPrefix = root.endsWith(sep) ? root : root + sep;
158
+ if (!filePath.startsWith(rootPrefix)) {
159
+ return null;
160
+ }
161
+ return filePath;
162
+ }
163
+ /**
164
+ * The result for a candidate file: found when it exists and is a file.
165
+ */
166
+ async function fileResult(candidate) {
167
+ if (candidate !== null) {
168
+ try {
169
+ if ((await stat(candidate)).isFile()) {
170
+ return { filePath: candidate, found: true };
171
+ }
172
+ }
173
+ catch {
174
+ // Not there
175
+ }
176
+ }
177
+ return { filePath: null, found: false };
178
+ }
62
179
  /**
63
180
  * Attempts to resolve pretty URL paths by trying various fallback strategies.
64
181
  * This handles common patterns like:
@@ -67,16 +184,20 @@ export function assertPortAvailable(port, host, configKey) {
67
184
  * - /path -> /path.html (when original path is not found and has no extension)
68
185
  *
69
186
  * @param outDir The output directory to serve files from
70
- * @param requestPath The requested URL path
71
- * @param originalFilePath The original file path that was attempted
187
+ * @param requestPath The requested URL path, which decides the fallbacks
188
+ * @param requestFile The file the request names, from {@link resolveRequestFile}. Every
189
+ * candidate is derived from it, so none is outside the output directory
72
190
  * @returns Promise resolving to the file path to serve or null if not found
73
191
  */
74
- export async function resolvePrettyUrl(outDir, requestPath, originalFilePath) {
192
+ export async function resolvePrettyUrl(outDir, requestPath, requestFile) {
193
+ // The sibling .html file: /blog/ and /blog are served from blog.html. The output
194
+ // directory itself has none, as its sibling is outside it.
195
+ const htmlFile = requestFile === resolve(outDir) ? null : `${requestFile}.html`;
75
196
  try {
76
- const stats = await stat(originalFilePath);
197
+ const stats = await stat(requestFile);
77
198
  if (stats.isDirectory()) {
78
199
  // Try to serve index.html from directory
79
- const indexPath = join(originalFilePath, 'index.html');
200
+ const indexPath = join(requestFile, 'index.html');
80
201
  try {
81
202
  await stat(indexPath);
82
203
  return { filePath: indexPath, found: true };
@@ -84,50 +205,17 @@ export async function resolvePrettyUrl(outDir, requestPath, originalFilePath) {
84
205
  catch {
85
206
  // If no index.html in directory, try to serve corresponding .html file
86
207
  // For example: /examples/ -> examples.html
87
- const directoryName = requestPath.replace(/\/$/, ''); // Remove trailing slash
88
- const fallbackPath = join(outDir, `${directoryName}.html`);
89
- try {
90
- await stat(fallbackPath);
91
- return { filePath: fallbackPath, found: true };
92
- }
93
- catch {
94
- return { filePath: null, found: false };
95
- }
208
+ return await fileResult(htmlFile);
96
209
  }
97
210
  }
98
211
  // File exists and is not a directory
99
- return { filePath: originalFilePath, found: true };
212
+ return { filePath: requestFile, found: true };
100
213
  }
101
214
  catch {
102
- // File not found, try fallback strategies for pretty URLs
103
- if (requestPath.endsWith('/')) {
104
- // For requests ending with /, try the corresponding .html file
105
- const pathWithoutSlash = requestPath.slice(0, -1);
106
- const htmlPath = join(outDir, `${pathWithoutSlash}.html`);
107
- try {
108
- const stats = await stat(htmlPath);
109
- if (stats.isFile()) {
110
- return { filePath: htmlPath, found: true };
111
- }
112
- }
113
- catch {
114
- // Continue to not found
115
- }
116
- }
117
- else if (!requestPath.includes('.')) {
118
- // For requests without trailing slash and without extension, try .html
119
- const htmlPath = join(outDir, `${requestPath}.html`);
120
- try {
121
- const stats = await stat(htmlPath);
122
- if (stats.isFile()) {
123
- return { filePath: htmlPath, found: true };
124
- }
125
- }
126
- catch {
127
- // Continue to not found
128
- }
215
+ // File not found: try the .html file for /path/ and for /path without an extension
216
+ if (requestPath.endsWith('/') || !requestPath.includes('.')) {
217
+ return await fileResult(htmlFile);
129
218
  }
130
- // No fallback worked
131
219
  return { filePath: null, found: false };
132
220
  }
133
221
  }
@@ -1,13 +1,37 @@
1
1
  import type { StatiConfig, PageModel } from '../../types/index.js';
2
+ /**
3
+ * Shared template discovery utilities.
4
+ * Extracted from templates.ts and isg/deps.ts to eliminate duplication.
5
+ */
6
+ /**
7
+ * Whether a page was created by Stati rather than loaded from a Markdown file: a page expanded
8
+ * from a `[param].md` template, or page 2 or later of a paginated listing.
9
+ *
10
+ * Virtual pages are never collection members, never make a folder a collection, and are left
11
+ * out of navigation, search and RSS unless they opt in.
12
+ *
13
+ * @param page - The page to check
14
+ * @returns True for generated pages and for pagination pages after the first
15
+ */
16
+ export declare function isVirtualPage(page: PageModel): boolean;
17
+ /**
18
+ * Finds the first page of a paginated listing, which later pages render like.
19
+ *
20
+ * @param page - A page with `pagination`
21
+ * @param allPages - All pages in the site
22
+ * @returns The listing's first page, or undefined when it is not in the list
23
+ */
24
+ export declare function getFirstPaginationPage(page: PageModel, allPages: PageModel[]): PageModel | undefined;
2
25
  /**
3
26
  * Determines if the given page is a collection index page.
4
27
  * A collection index page is one whose URL matches a directory path that contains other pages.
28
+ * Generated pages never are; pagination pages after the first are when their first page is.
5
29
  *
6
30
  * Rendering and ISG dependency tracking must both call this with the same page list,
7
31
  * otherwise they can pick different layouts for the same page.
8
32
  *
9
33
  * Page URLs are read once per `allPages` array, at the first call; see
10
- * `directoryPrefixCache` for when changes to the pages are picked up.
34
+ * `pageListCache` for when changes to the pages are picked up.
11
35
  *
12
36
  * @param page - The page to check
13
37
  * @param allPages - All pages in the site (treated as unchanging after the first call)