@stati/core 1.20.1 → 1.20.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":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA4B7D,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;AAuXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAudxF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA6B7D,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;AAuXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAgexF"}
package/dist/core/dev.js CHANGED
@@ -9,7 +9,7 @@ import { loadConfig } from '../config/loader.js';
9
9
  import { loadCacheManifest, saveCacheManifest, computeNavigationHash } from './isg/index.js';
10
10
  import { loadContent } from './content.js';
11
11
  import { buildNavigation } from './navigation.js';
12
- import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, normalizePathForComparison, } from './utils/index.js';
12
+ import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, normalizePathForComparison, isPathWithinDirectory, } from './utils/index.js';
13
13
  import { setEnv, getEnv } from '../env.js';
14
14
  import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION, DEFAULT_OUT_DIR, } from '../constants.js';
15
15
  /**
@@ -446,6 +446,14 @@ export async function createDevServer(options = {}) {
446
446
  };
447
447
  }
448
448
  const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
449
+ // Security: Prevent path traversal attacks
450
+ if (!isPathWithinDirectory(outDir, originalFilePath)) {
451
+ return {
452
+ content: '403 - Forbidden',
453
+ mimeType: 'text/plain',
454
+ statusCode: 403,
455
+ };
456
+ }
449
457
  // Use the shared pretty URL resolver
450
458
  const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
451
459
  if (!found || !filePath) {
@@ -7,14 +7,18 @@ export declare class CircularDependencyError extends Error {
7
7
  constructor(dependencyChain: string[], message: string);
8
8
  }
9
9
  /**
10
- * Tracks all template dependencies for a given page.
11
- * This includes the layout file and all accessible partials.
12
- * Includes circular dependency detection.
10
+ * Tracks template dependencies for a given page by parsing template content.
11
+ * Only includes templates that are actually referenced (via include, layout, or stati.partials calls).
12
+ * This provides accurate dependency tracking for ISG cache invalidation.
13
+ *
14
+ * The function recursively parses the layout template and all its dependencies to build
15
+ * the complete dependency tree. This ensures that changes to unused partials don't
16
+ * trigger unnecessary page rebuilds.
13
17
  *
14
18
  * @param page - The page model to track dependencies for
15
19
  * @param config - Stati configuration
16
- * @returns Array of absolute paths to dependency files
17
- * @throws {CircularDependencyError} When circular dependencies are detected
20
+ * @returns Array of absolute paths to actually-used template files (POSIX format)
21
+ * @throws {CircularDependencyError} When circular dependencies are detected in templates
18
22
  *
19
23
  * @example
20
24
  * ```typescript
@@ -29,20 +33,6 @@ export declare class CircularDependencyError extends Error {
29
33
  * ```
30
34
  */
31
35
  export declare function trackTemplateDependencies(page: PageModel, config: StatiConfig): Promise<string[]>;
32
- /**
33
- * Finds all partial dependencies for a given page path.
34
- * Searches up the directory hierarchy for _* folders containing .eta files.
35
- *
36
- * @param pagePath - Relative path to the page from srcDir
37
- * @param config - Stati configuration
38
- * @returns Array of absolute paths to partial files
39
- *
40
- * @example
41
- * ```typescript
42
- * const partials = await findPartialDependencies('blog/post.md', config);
43
- * ```
44
- */
45
- export declare function findPartialDependencies(pagePath: string, config: StatiConfig): Promise<string[]>;
46
36
  /**
47
37
  * Resolves a template name to its file path.
48
38
  * Used for explicit layout specifications in front matter.
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnE;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoCnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiDnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnE;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CA+CnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
@@ -14,14 +14,18 @@ export class CircularDependencyError extends Error {
14
14
  }
15
15
  }
16
16
  /**
17
- * Tracks all template dependencies for a given page.
18
- * This includes the layout file and all accessible partials.
19
- * Includes circular dependency detection.
17
+ * Tracks template dependencies for a given page by parsing template content.
18
+ * Only includes templates that are actually referenced (via include, layout, or stati.partials calls).
19
+ * This provides accurate dependency tracking for ISG cache invalidation.
20
+ *
21
+ * The function recursively parses the layout template and all its dependencies to build
22
+ * the complete dependency tree. This ensures that changes to unused partials don't
23
+ * trigger unnecessary page rebuilds.
20
24
  *
21
25
  * @param page - The page model to track dependencies for
22
26
  * @param config - Stati configuration
23
- * @returns Array of absolute paths to dependency files
24
- * @throws {CircularDependencyError} When circular dependencies are detected
27
+ * @returns Array of absolute paths to actually-used template files (POSIX format)
28
+ * @throws {CircularDependencyError} When circular dependencies are detected in templates
25
29
  *
26
30
  * @example
27
31
  * ```typescript
@@ -45,75 +49,28 @@ export async function trackTemplateDependencies(page, config) {
45
49
  const srcDir = resolveSrcDir(config);
46
50
  const relativePath = relative(srcDir, page.sourcePath);
47
51
  // Track dependencies with circular detection
52
+ // The visited set will contain all templates that are actually used (not just accessible)
48
53
  const visited = new Set();
49
54
  const currentPath = new Set();
50
55
  // 1. Find the layout file that will be used for this page
51
56
  const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isCollectionIndexPage(page));
52
57
  if (layoutPath) {
53
- const absoluteLayoutPath = join(srcDir, layoutPath);
58
+ // Normalize to POSIX format for consistent manifest output across platforms
59
+ const absoluteLayoutPath = posix.join(srcDir.replace(/\\/g, '/'), layoutPath);
54
60
  deps.push(absoluteLayoutPath);
55
- // Check for circular dependencies in layout chain
56
- await detectCircularDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
61
+ // Recursively traverse the template dependency tree
62
+ // This populates 'visited' with all actually-used templates (not just accessible ones)
63
+ await collectTemplateDependencies(absoluteLayoutPath, srcDir, visited, currentPath);
57
64
  }
58
- // 2. Find all partials accessible to this page
59
- const partialDeps = await findPartialDependencies(relativePath, config);
60
- deps.push(...partialDeps);
61
- return deps;
62
- }
63
- /**
64
- * Finds all partial dependencies for a given page path.
65
- * Searches up the directory hierarchy for _* folders containing .eta files.
66
- *
67
- * @param pagePath - Relative path to the page from srcDir
68
- * @param config - Stati configuration
69
- * @returns Array of absolute paths to partial files
70
- *
71
- * @example
72
- * ```typescript
73
- * const partials = await findPartialDependencies('blog/post.md', config);
74
- * ```
75
- */
76
- export async function findPartialDependencies(pagePath, config) {
77
- // Early return if required config values are missing
78
- if (!config.srcDir) {
79
- console.warn('Config srcDir is missing, cannot find partial dependencies');
80
- return [];
81
- }
82
- const deps = [];
83
- const srcDir = resolveSrcDir(config);
84
- // Get the directory of the current page
85
- const pageDir = dirname(pagePath);
86
- const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
87
- // Build list of directories to search (current dir up to root)
88
- const dirsToSearch = [];
89
- // Add directories from current up to root
90
- if (pathSegments.length > 0) {
91
- for (let i = pathSegments.length; i >= 0; i--) {
92
- if (i === 0) {
93
- dirsToSearch.push(''); // Root directory
94
- }
95
- else {
96
- dirsToSearch.push(pathSegments.slice(0, i).join('/'));
97
- }
98
- }
99
- }
100
- else {
101
- dirsToSearch.push(''); // Root directory only
102
- }
103
- // Search each directory for _* folders containing .eta files
104
- for (const dir of dirsToSearch) {
105
- const searchDir = dir ? join(srcDir, dir) : srcDir;
106
- try {
107
- // Find all .eta files in _* subdirectories
108
- // Use posix.join to ensure forward slashes for glob patterns
109
- const normalizedSearchDir = searchDir.replace(/\\/g, '/');
110
- const pattern = posix.join(normalizedSearchDir, '_*/**/*.eta');
111
- const partialFiles = await glob(pattern, { absolute: true });
112
- deps.push(...partialFiles);
113
- }
114
- catch {
115
- // Continue if directory doesn't exist or can't be read
116
- continue;
65
+ // 2. Add all actually-used templates from the visited set
66
+ // Filter to only include underscore directories (partials/components)
67
+ // and normalize paths to POSIX format
68
+ for (const templatePath of visited) {
69
+ const normalizedPath = templatePath.replace(/\\/g, '/');
70
+ // Only add partials (files in directories starting with underscore) - layout is already added
71
+ // Use strict pattern to match /_dirname/ where dirname starts with underscore
72
+ if (/\/_[^/]+\//.test(normalizedPath)) {
73
+ deps.push(normalizedPath);
117
74
  }
118
75
  }
119
76
  return deps;
@@ -143,59 +100,63 @@ export async function resolveTemplatePath(layout, config) {
143
100
  return null;
144
101
  }
145
102
  /**
146
- * Detects circular dependencies in template includes/extends.
147
- * Uses DFS to traverse the dependency graph and detect cycles.
103
+ * Recursively collects all template dependencies by parsing template content.
104
+ * Only includes templates that are actually referenced (not just accessible).
105
+ * Uses DFS to traverse the dependency graph and detects circular references.
148
106
  *
149
- * @param templatePath - Absolute path to template file to check
107
+ * @param templatePath - Absolute path to template file to analyze
150
108
  * @param srcDir - Source directory for resolving relative template paths
151
- * @param visited - Set of all visited template paths (for optimization)
109
+ * @param visited - Set to track all visited templates (accumulated dependencies)
152
110
  * @param currentPath - Set of templates in current DFS path (for cycle detection)
153
111
  * @throws {CircularDependencyError} When a circular dependency is detected
154
112
  */
155
- async function detectCircularDependencies(templatePath, srcDir, visited, currentPath) {
156
- // Skip if already processed
157
- if (visited.has(templatePath)) {
158
- return;
159
- }
160
- // Check for circular dependency
161
- if (currentPath.has(templatePath)) {
162
- const chain = Array.from(currentPath);
163
- chain.push(templatePath);
113
+ async function collectTemplateDependencies(templatePath, srcDir, visited, currentPath) {
114
+ // Normalize path for consistent tracking
115
+ const normalizedPath = templatePath.replace(/\\/g, '/');
116
+ // Check for circular dependency FIRST (before visited check)
117
+ // A circular dependency means we're visiting a template that's already in our current DFS path
118
+ // This must be checked before the visited check because a path in currentPath is always in visited
119
+ if (currentPath.has(normalizedPath)) {
120
+ const chain = [...currentPath, normalizedPath];
164
121
  throw new CircularDependencyError(chain, `Circular dependency detected in templates: ${chain.join(' -> ')}`);
165
122
  }
123
+ // Skip if already processed (but not in current path - those are handled above)
124
+ if (visited.has(normalizedPath)) {
125
+ return;
126
+ }
166
127
  // Check if template file exists
167
128
  if (!(await pathExists(templatePath))) {
168
- // Don't treat missing files as circular dependencies
169
- // They will be handled by the missing file error handling
170
129
  return;
171
130
  }
172
- // Add to current path
173
- currentPath.add(templatePath);
174
- visited.add(templatePath);
131
+ // Add to tracking sets
132
+ currentPath.add(normalizedPath);
133
+ visited.add(normalizedPath);
175
134
  try {
176
135
  // Read template content to find includes/extends
177
136
  const content = await readFile(templatePath, 'utf-8');
178
137
  if (!content) {
179
- return; // Skip if file doesn't exist
138
+ return;
180
139
  }
140
+ // Parse template to find referenced templates
181
141
  const dependencies = await parseTemplateDependencies(content, templatePath, srcDir);
182
- // Recursively check dependencies
142
+ // Recursively collect dependencies
183
143
  for (const depPath of dependencies) {
184
- await detectCircularDependencies(depPath, srcDir, visited, currentPath);
144
+ await collectTemplateDependencies(depPath, srcDir, visited, currentPath);
185
145
  }
186
146
  }
187
147
  catch (error) {
188
- // If we can't read the file, don't treat it as a circular dependency
189
- if (error instanceof Error && !error.message.includes('Circular dependency')) {
190
- console.warn(`Warning: Could not read template ${templatePath}: ${error.message}`);
148
+ // Re-throw circular dependency errors - these are fatal
149
+ if (error instanceof CircularDependencyError) {
150
+ throw error;
191
151
  }
192
- else {
193
- throw error; // Re-throw circular dependency errors
152
+ // Log warning but continue - don't fail the entire build for template parsing issues
153
+ if (error instanceof Error) {
154
+ console.warn(`Warning: Could not parse template ${templatePath}: ${error.message}`);
194
155
  }
195
156
  }
196
157
  finally {
197
158
  // Remove from current path when backtracking
198
- currentPath.delete(templatePath);
159
+ currentPath.delete(normalizedPath);
199
160
  }
200
161
  }
201
162
  /**
@@ -244,27 +205,52 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
244
205
  }
245
206
  }
246
207
  }
247
- // Look for Stati callable partial patterns: stati.partials.name( or stati.partials['name'](
248
- // This catches both direct property access and bracket notation with or without arguments
249
- // Patterns allow for optional whitespace before the opening parenthesis
250
- const callablePartialPatterns = [
251
- /stati\.partials\.(\w+)\s*\(/g, // stati.partials.header( or stati.partials.header (
252
- /stati\.partials\[['"`]([^'"`]+)['"`]\]\s*\(/g, // stati.partials['header']( with whitespace
208
+ // Look for Stati partial patterns - both callable and non-callable
209
+ // Callable: stati.partials.name() or stati.partials['name']()
210
+ // Non-callable: stati.partials.name or stati.partials['name'] (used with <%~ %>)
211
+ const partialPatterns = [
212
+ // Callable patterns (with parentheses)
213
+ /stati\.partials\.(\w+)\s*\(/g, // stati.partials.header(
214
+ /stati\.partials\[['"`]([^'"`]+)['"`]\]\s*\(/g, // stati.partials['header'](
215
+ // Non-callable patterns (without parentheses, used in <%~ stati.partials.name %>)
216
+ // These patterns use lookaheads to distinguish between:
217
+ // - stati.partials.name (partial reference, should match)
218
+ // - stati.partials.name() (function call, should NOT match here - handled above)
219
+ //
220
+ // Pattern breakdown for dot notation: /stati\.partials\.(\w+)(?=\s*[%}\s]|$)(?!\s*\()/g
221
+ // - stati\.partials\. : Literal "stati.partials."
222
+ // - (\w+) : Capture partial name (letters, digits, underscore)
223
+ // - (?=\s*[%}\s]|$) : Positive lookahead - must be followed by whitespace, %, }, or end of string
224
+ // - (?!\s*\() : Negative lookahead - must NOT be followed by "(" (excludes function calls)
225
+ /stati\.partials\.(\w+)(?=\s*[%}\s]|$)(?!\s*\()/g,
226
+ // Pattern breakdown for bracket notation: /stati\.partials\[['"`]([^'"`]+)['"`]\](?=\s*[%}\s]|$)(?!\s*\()/g
227
+ // - stati\.partials\[ : Literal "stati.partials["
228
+ // - ['"`] : Opening quote (single, double, or backtick)
229
+ // - ([^'"`]+) : Capture partial name (any chars except quotes)
230
+ // - ['"`]\] : Closing quote and bracket
231
+ // - (?=\s*[%}\s]|$) : Positive lookahead - must be followed by whitespace, %, }, or end of string
232
+ // - (?!\s*\() : Negative lookahead - must NOT be followed by "(" (excludes function calls)
233
+ /stati\.partials\[['"`]([^'"`]+)['"`]\](?=\s*[%}\s]|$)(?!\s*\()/g,
253
234
  ];
254
- for (const pattern of callablePartialPatterns) {
235
+ // Use a Set to avoid duplicate partial names
236
+ const foundPartials = new Set();
237
+ for (const pattern of partialPatterns) {
255
238
  let match;
256
239
  while ((match = pattern.exec(content)) !== null) {
257
240
  const partialName = match[1];
258
241
  if (partialName) {
259
- // Resolve the partial by searching for it in underscore directories
260
- const partialFileName = `${partialName}${TEMPLATE_EXTENSION}`;
261
- const resolvedPath = await resolveTemplatePathInternal(partialFileName, srcDir, templateDir);
262
- if (resolvedPath) {
263
- dependencies.push(resolvedPath);
264
- }
242
+ foundPartials.add(partialName);
265
243
  }
266
244
  }
267
245
  }
246
+ // Resolve each unique partial name
247
+ for (const partialName of foundPartials) {
248
+ const partialFileName = `${partialName}${TEMPLATE_EXTENSION}`;
249
+ const resolvedPath = await resolveTemplatePathInternal(partialFileName, srcDir, templateDir);
250
+ if (resolvedPath) {
251
+ dependencies.push(resolvedPath);
252
+ }
253
+ }
268
254
  return dependencies;
269
255
  }
270
256
  /**
@@ -320,7 +306,8 @@ async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
320
306
  const pattern = posix.join(searchDir.replace(/\\/g, '/'), '_*', templateName);
321
307
  const matches = await glob(pattern, { absolute: true });
322
308
  if (matches.length > 0) {
323
- return matches[0]; // Return first match
309
+ // Normalize to POSIX format for consistent cross-platform path handling
310
+ return matches[0].replace(/\\/g, '/');
324
311
  }
325
312
  }
326
313
  catch {
@@ -9,7 +9,7 @@
9
9
  export { loadCacheManifest, saveCacheManifest, createEmptyManifest } from './manifest.js';
10
10
  export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder.js';
11
11
  export { BuildLockManager, withBuildLock } from './build-lock.js';
12
- export { CircularDependencyError, trackTemplateDependencies, findPartialDependencies, resolveTemplatePath, } from './deps.js';
12
+ export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath } from './deps.js';
13
13
  export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, } from './hash.js';
14
14
  export { getSafeCurrentTime, parseSafeDate, computeEffectiveTTL, computeNextRebuildAt, isPageFrozen, applyAgingRules, } from './ttl.js';
15
15
  export { ISGConfigurationError, validateISGConfig, validatePageISGOverrides, extractNumericOverride, } from './validation.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EACL,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/isg/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrF,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGpG,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC"}
@@ -13,7 +13,7 @@ export { shouldRebuildPage, createCacheEntry, updateCacheEntry } from './builder
13
13
  // Build locking
14
14
  export { BuildLockManager, withBuildLock } from './build-lock.js';
15
15
  // Dependency tracking
16
- export { CircularDependencyError, trackTemplateDependencies, findPartialDependencies, resolveTemplatePath, } from './deps.js';
16
+ export { CircularDependencyError, trackTemplateDependencies, resolveTemplatePath } from './deps.js';
17
17
  // Hash computation
18
18
  export { computeContentHash, computeFileHash, computeInputsHash, computeNavigationHash, } from './hash.js';
19
19
  // TTL and aging
@@ -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;AAU7D,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
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,mBAAmB,CAAC;AAW7D,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,CAiKxB"}
@@ -2,7 +2,7 @@ import { createServer } from 'node:http';
2
2
  import { join, extname } from 'node:path';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { loadConfig } from '../config/loader.js';
5
- import { resolveDevPaths, resolvePrettyUrl, createFallbackLogger, mergeServerOptions, } from './utils/index.js';
5
+ import { resolveDevPaths, resolvePrettyUrl, createFallbackLogger, mergeServerOptions, isPathWithinDirectory, } from './utils/index.js';
6
6
  import { DEFAULT_PREVIEW_PORT, DEFAULT_DEV_HOST } from '../constants.js';
7
7
  /**
8
8
  * Loads and validates configuration for the preview server.
@@ -70,6 +70,14 @@ export async function createPreviewServer(options = {}) {
70
70
  */
71
71
  async function serveFile(requestPath) {
72
72
  const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
73
+ // Security: Prevent path traversal attacks
74
+ if (!isPathWithinDirectory(outDir, originalFilePath)) {
75
+ return {
76
+ content: '403 - Forbidden',
77
+ mimeType: 'text/plain',
78
+ statusCode: 403,
79
+ };
80
+ }
73
81
  // Use the shared pretty URL resolver
74
82
  const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
75
83
  if (!found || !filePath) {
@@ -3,7 +3,7 @@
3
3
  * @module core/utils
4
4
  */
5
5
  export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
6
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, } from './paths.utils.js';
6
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
7
7
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
8
8
  export { propValue } from './template.utils.js';
9
9
  export { slugify } from './slugify.utils.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,GACL,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAG9E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,YAAY,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG7D,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -5,7 +5,7 @@
5
5
  // File system utilities
6
6
  export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir, stat, } from './fs.utils.js';
7
7
  // Path resolution utilities
8
- export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, } from './paths.utils.js';
8
+ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, normalizePathForComparison, isPathWithinDirectory, } from './paths.utils.js';
9
9
  // Template discovery utilities
10
10
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.utils.js';
11
11
  // Template utilities
@@ -91,4 +91,26 @@ export declare function resolveStaticPath(config: StatiConfig, relativePath: str
91
91
  * ```
92
92
  */
93
93
  export declare function normalizePathForComparison(filePath: string, basePath?: string): string;
94
+ /**
95
+ * Validates that a resolved path is safely within a base directory.
96
+ * This function prevents path traversal attacks by ensuring that a target path
97
+ * (which may contain `..` sequences or other traversal patterns) resolves to
98
+ * a location within the specified base directory.
99
+ *
100
+ * @param baseDir - The base directory that the target path must stay within
101
+ * @param targetPath - The target path to validate (can be relative or contain `..`)
102
+ * @returns `true` if the resolved target path is within the base directory, `false` otherwise
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Safe paths
107
+ * isPathWithinDirectory('/app/dist', '/app/dist/index.html') // true
108
+ * isPathWithinDirectory('/app/dist', '/app/dist/pages/about.html') // true
109
+ *
110
+ * // Path traversal attempts (returns false)
111
+ * isPathWithinDirectory('/app/dist', '/app/dist/../../../etc/passwd') // false
112
+ * isPathWithinDirectory('/app/dist', '/../etc/passwd') // false
113
+ * ```
114
+ */
115
+ export declare function isPathWithinDirectory(baseDir: string, targetPath: string): boolean;
94
116
  //# 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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAuBtF"}
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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAuBtF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYlF"}
@@ -1,4 +1,4 @@
1
- import { join, posix } from 'node:path';
1
+ import { join, posix, resolve, sep } from 'node:path';
2
2
  import { DEFAULT_SRC_DIR, DEFAULT_OUT_DIR, DEFAULT_STATIC_DIR, CACHE_DIR_NAME, } from '../../constants.js';
3
3
  /**
4
4
  * File system path resolution utilities for Stati core.
@@ -128,3 +128,35 @@ export function normalizePathForComparison(filePath, basePath) {
128
128
  }
129
129
  return normalized;
130
130
  }
131
+ /**
132
+ * Validates that a resolved path is safely within a base directory.
133
+ * This function prevents path traversal attacks by ensuring that a target path
134
+ * (which may contain `..` sequences or other traversal patterns) resolves to
135
+ * a location within the specified base directory.
136
+ *
137
+ * @param baseDir - The base directory that the target path must stay within
138
+ * @param targetPath - The target path to validate (can be relative or contain `..`)
139
+ * @returns `true` if the resolved target path is within the base directory, `false` otherwise
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Safe paths
144
+ * isPathWithinDirectory('/app/dist', '/app/dist/index.html') // true
145
+ * isPathWithinDirectory('/app/dist', '/app/dist/pages/about.html') // true
146
+ *
147
+ * // Path traversal attempts (returns false)
148
+ * isPathWithinDirectory('/app/dist', '/app/dist/../../../etc/passwd') // false
149
+ * isPathWithinDirectory('/app/dist', '/../etc/passwd') // false
150
+ * ```
151
+ */
152
+ export function isPathWithinDirectory(baseDir, targetPath) {
153
+ // Resolve both paths to absolute paths with all `..` and `.` resolved
154
+ const resolvedBase = resolve(baseDir);
155
+ const resolvedTarget = resolve(targetPath);
156
+ // Normalize the base directory to ensure proper prefix matching
157
+ // Adding sep ensures '/app/dist' doesn't match '/app/dist-other'
158
+ const normalizedBase = resolvedBase.endsWith(sep) ? resolvedBase : resolvedBase + sep;
159
+ // Check if the resolved target starts with the normalized base
160
+ // We also allow exact match (resolvedTarget === resolvedBase) for the root
161
+ return resolvedTarget === resolvedBase || resolvedTarget.startsWith(normalizedBase);
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.20.1",
3
+ "version": "1.20.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",