@stati/core 1.20.0 → 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.
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +25 -10
- package/dist/core/isg/deps.d.ts +9 -19
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +95 -108
- package/dist/core/isg/index.d.ts +1 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +1 -1
- package/dist/core/preview.d.ts.map +1 -1
- package/dist/core/preview.js +9 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/paths.utils.d.ts +49 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +78 -1
- package/package.json +1 -1
package/dist/core/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { join, extname
|
|
2
|
+
import { join, extname } from 'node:path';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { WebSocketServer } from 'ws';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
@@ -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, } 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
|
/**
|
|
@@ -180,7 +180,9 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
|
-
* Handles template/partial file changes by invalidating affected pages
|
|
183
|
+
* Handles template/partial file changes by invalidating affected pages.
|
|
184
|
+
* Uses proper path normalization to ensure reliable matching between
|
|
185
|
+
* file watcher paths and cached dependency paths.
|
|
184
186
|
*/
|
|
185
187
|
async function handleTemplateChange(templatePath, configPath, logger) {
|
|
186
188
|
const cacheDir = resolveCacheDir();
|
|
@@ -197,16 +199,21 @@ async function handleTemplateChange(templatePath, configPath, logger) {
|
|
|
197
199
|
});
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
202
|
+
// Normalize the changed template path to absolute POSIX format for reliable comparison
|
|
203
|
+
// This handles cases where the watcher provides relative paths, Windows paths, or different
|
|
204
|
+
// path representations than what's stored in the cache manifest
|
|
205
|
+
const normalizedTemplatePath = normalizePathForComparison(templatePath);
|
|
200
206
|
// Find pages that depend on this template
|
|
201
207
|
let affectedPagesCount = 0;
|
|
202
|
-
const normalizedTemplatePath = posix.normalize(templatePath.replace(/\\/g, '/'));
|
|
203
208
|
for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
|
|
204
|
-
if
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
// Check if any of the page's dependencies match the changed template
|
|
210
|
+
const hasMatchingDep = entry.deps.some((dep) => {
|
|
211
|
+
// Normalize the cached dependency path to the same format
|
|
212
|
+
const normalizedDep = normalizePathForComparison(dep);
|
|
213
|
+
// Direct path comparison - both paths are now in consistent format
|
|
214
|
+
return normalizedDep === normalizedTemplatePath;
|
|
215
|
+
});
|
|
216
|
+
if (hasMatchingDep) {
|
|
210
217
|
affectedPagesCount++;
|
|
211
218
|
// Remove from cache to force rebuild
|
|
212
219
|
delete cacheManifest.entries[pagePath];
|
|
@@ -439,6 +446,14 @@ export async function createDevServer(options = {}) {
|
|
|
439
446
|
};
|
|
440
447
|
}
|
|
441
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
|
+
}
|
|
442
457
|
// Use the shared pretty URL resolver
|
|
443
458
|
const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
|
|
444
459
|
if (!found || !filePath) {
|
package/dist/core/isg/deps.d.ts
CHANGED
|
@@ -7,14 +7,18 @@ export declare class CircularDependencyError extends Error {
|
|
|
7
7
|
constructor(dependencyChain: string[], message: string);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Tracks
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
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"}
|
package/dist/core/isg/deps.js
CHANGED
|
@@ -14,14 +14,18 @@ export class CircularDependencyError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Tracks
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
56
|
-
|
|
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.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
*
|
|
147
|
-
*
|
|
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
|
|
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
|
|
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
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
if (currentPath.has(
|
|
162
|
-
const chain =
|
|
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
|
|
173
|
-
currentPath.add(
|
|
174
|
-
visited.add(
|
|
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;
|
|
138
|
+
return;
|
|
180
139
|
}
|
|
140
|
+
// Parse template to find referenced templates
|
|
181
141
|
const dependencies = await parseTemplateDependencies(content, templatePath, srcDir);
|
|
182
|
-
// Recursively
|
|
142
|
+
// Recursively collect dependencies
|
|
183
143
|
for (const depPath of dependencies) {
|
|
184
|
-
await
|
|
144
|
+
await collectTemplateDependencies(depPath, srcDir, visited, currentPath);
|
|
185
145
|
}
|
|
186
146
|
}
|
|
187
147
|
catch (error) {
|
|
188
|
-
//
|
|
189
|
-
if (error instanceof
|
|
190
|
-
|
|
148
|
+
// Re-throw circular dependency errors - these are fatal
|
|
149
|
+
if (error instanceof CircularDependencyError) {
|
|
150
|
+
throw error;
|
|
191
151
|
}
|
|
192
|
-
|
|
193
|
-
|
|
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(
|
|
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
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
/stati\.partials\
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
// Normalize to POSIX format for consistent cross-platform path handling
|
|
310
|
+
return matches[0].replace(/\\/g, '/');
|
|
324
311
|
}
|
|
325
312
|
}
|
|
326
313
|
catch {
|
package/dist/core/isg/index.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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"}
|
package/dist/core/isg/index.js
CHANGED
|
@@ -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,
|
|
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;
|
|
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"}
|
package/dist/core/preview.js
CHANGED
|
@@ -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, } 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,
|
|
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"}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -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, } 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
|
|
@@ -64,4 +64,53 @@ export declare function resolveOutPath(config: StatiConfig, relativePath: string
|
|
|
64
64
|
* @returns Absolute path
|
|
65
65
|
*/
|
|
66
66
|
export declare function resolveStaticPath(config: StatiConfig, relativePath: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Normalizes a file path to absolute POSIX format for consistent comparisons.
|
|
69
|
+
* Handles Windows paths, relative paths, and already-normalized paths.
|
|
70
|
+
*
|
|
71
|
+
* This utility ensures that paths from different sources (file watchers, cache manifest,
|
|
72
|
+
* glob results) can be reliably compared even when they use different representations.
|
|
73
|
+
*
|
|
74
|
+
* @param filePath - Path to normalize (can be relative or absolute, Windows or POSIX)
|
|
75
|
+
* @param basePath - Optional base path to resolve relative paths against (defaults to cwd)
|
|
76
|
+
* @returns Normalized absolute path with forward slashes and no trailing slashes
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* // Windows absolute path
|
|
81
|
+
* normalizePathForComparison('C:\\project\\site\\layout.eta')
|
|
82
|
+
* // Returns: 'C:/project/site/layout.eta'
|
|
83
|
+
*
|
|
84
|
+
* // Relative path
|
|
85
|
+
* normalizePathForComparison('site/layout.eta', '/project')
|
|
86
|
+
* // Returns: '/project/site/layout.eta'
|
|
87
|
+
*
|
|
88
|
+
* // Already POSIX path
|
|
89
|
+
* normalizePathForComparison('/project/site/layout.eta')
|
|
90
|
+
* // Returns: '/project/site/layout.eta'
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
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;
|
|
67
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"}
|
|
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.
|
|
@@ -83,3 +83,80 @@ export function resolveOutPath(config, relativePath) {
|
|
|
83
83
|
export function resolveStaticPath(config, relativePath) {
|
|
84
84
|
return join(resolveStaticDir(config), relativePath);
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Normalizes a file path to absolute POSIX format for consistent comparisons.
|
|
88
|
+
* Handles Windows paths, relative paths, and already-normalized paths.
|
|
89
|
+
*
|
|
90
|
+
* This utility ensures that paths from different sources (file watchers, cache manifest,
|
|
91
|
+
* glob results) can be reliably compared even when they use different representations.
|
|
92
|
+
*
|
|
93
|
+
* @param filePath - Path to normalize (can be relative or absolute, Windows or POSIX)
|
|
94
|
+
* @param basePath - Optional base path to resolve relative paths against (defaults to cwd)
|
|
95
|
+
* @returns Normalized absolute path with forward slashes and no trailing slashes
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // Windows absolute path
|
|
100
|
+
* normalizePathForComparison('C:\\project\\site\\layout.eta')
|
|
101
|
+
* // Returns: 'C:/project/site/layout.eta'
|
|
102
|
+
*
|
|
103
|
+
* // Relative path
|
|
104
|
+
* normalizePathForComparison('site/layout.eta', '/project')
|
|
105
|
+
* // Returns: '/project/site/layout.eta'
|
|
106
|
+
*
|
|
107
|
+
* // Already POSIX path
|
|
108
|
+
* normalizePathForComparison('/project/site/layout.eta')
|
|
109
|
+
* // Returns: '/project/site/layout.eta'
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function normalizePathForComparison(filePath, basePath) {
|
|
113
|
+
// Convert backslashes to forward slashes for Windows compatibility
|
|
114
|
+
let normalized = filePath.replace(/\\/g, '/');
|
|
115
|
+
// Resolve to absolute path if relative
|
|
116
|
+
// Check if path is already absolute (starts with / or drive letter)
|
|
117
|
+
const isAbsolute = normalized.startsWith('/') || /^[a-zA-Z]:/.test(normalized);
|
|
118
|
+
if (!isAbsolute) {
|
|
119
|
+
const base = basePath || process.cwd();
|
|
120
|
+
// Use the already-normalized path to avoid reintroducing backslashes
|
|
121
|
+
normalized = join(base, normalized).replace(/\\/g, '/');
|
|
122
|
+
}
|
|
123
|
+
// Use posix.normalize to clean up any '..' or '.' segments and remove redundant separators
|
|
124
|
+
normalized = posix.normalize(normalized);
|
|
125
|
+
// Remove trailing slash (except for root path)
|
|
126
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
127
|
+
normalized = normalized.slice(0, -1);
|
|
128
|
+
}
|
|
129
|
+
return normalized;
|
|
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
|
+
}
|