@stati/core 1.8.0 → 1.10.0

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 (42) hide show
  1. package/README.md +6 -6
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +39 -4
  4. package/dist/core/dev.d.ts.map +1 -1
  5. package/dist/core/dev.js +70 -2
  6. package/dist/core/isg/deps.js +21 -0
  7. package/dist/core/isg/hash.d.ts +14 -0
  8. package/dist/core/isg/hash.d.ts.map +1 -1
  9. package/dist/core/isg/hash.js +32 -1
  10. package/dist/core/isg/index.d.ts +1 -1
  11. package/dist/core/isg/index.d.ts.map +1 -1
  12. package/dist/core/isg/index.js +1 -1
  13. package/dist/core/isg/manifest.d.ts.map +1 -1
  14. package/dist/core/isg/manifest.js +7 -1
  15. package/dist/core/templates.d.ts.map +1 -1
  16. package/dist/core/templates.js +43 -8
  17. package/dist/core/utils/callable-partials.d.ts +60 -0
  18. package/dist/core/utils/callable-partials.d.ts.map +1 -0
  19. package/dist/core/utils/callable-partials.js +108 -0
  20. package/dist/core/utils/index.d.ts +4 -0
  21. package/dist/core/utils/index.d.ts.map +1 -1
  22. package/dist/core/utils/index.js +6 -0
  23. package/dist/core/utils/navigation-helpers.d.ts +124 -0
  24. package/dist/core/utils/navigation-helpers.d.ts.map +1 -0
  25. package/dist/core/utils/navigation-helpers.js +219 -0
  26. package/dist/core/utils/partial-validation.d.ts +5 -2
  27. package/dist/core/utils/partial-validation.d.ts.map +1 -1
  28. package/dist/core/utils/partial-validation.js +35 -7
  29. package/dist/core/utils/server.d.ts +1 -1
  30. package/dist/core/utils/server.d.ts.map +1 -1
  31. package/dist/core/utils/server.js +14 -1
  32. package/dist/core/utils/tailwind-inventory.d.ts +91 -0
  33. package/dist/core/utils/tailwind-inventory.d.ts.map +1 -0
  34. package/dist/core/utils/tailwind-inventory.js +228 -0
  35. package/dist/core/utils/template-utils.d.ts +3 -0
  36. package/dist/core/utils/template-utils.d.ts.map +1 -1
  37. package/dist/core/utils/template-utils.js +27 -3
  38. package/dist/types/content.d.ts +24 -3
  39. package/dist/types/content.d.ts.map +1 -1
  40. package/dist/types/isg.d.ts +4 -1
  41. package/dist/types/isg.d.ts.map +1 -1
  42. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Creates a callable partial that can be used both as a value and as a function.
3
+ * This enables both syntaxes:
4
+ * - Direct usage: <%~ stati.partials.header %>
5
+ * - With props: <%~ stati.partials.hero({ title: 'Hello' }) %>
6
+ *
7
+ * @param eta - The Eta template engine instance
8
+ * @param partialPath - Absolute path to the partial template file
9
+ * @param baseContext - The base template context (without props)
10
+ * @param renderedContent - Pre-rendered content for the no-props case
11
+ * @returns A callable partial function
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const callable = makeCallablePartial(eta, '/path/to/partial.eta', baseContext, '<div>Header</div>');
16
+ *
17
+ * // Use without props (returns pre-rendered content)
18
+ * const html1 = callable.toString(); // '<div>Header</div>'
19
+ *
20
+ * // Use with props (re-renders with merged context)
21
+ * const html2 = callable({ title: 'Custom Title' }); // Renders with custom props
22
+ * ```
23
+ */
24
+ export function makeCallablePartial(eta, partialPath, baseContext, renderedContent) {
25
+ /**
26
+ * The main callable function.
27
+ * When called with props, re-renders the partial with merged context.
28
+ * When called without props, returns the pre-rendered content.
29
+ */
30
+ const callable = (props) => {
31
+ if (!props || Object.keys(props).length === 0) {
32
+ // No props provided - return pre-rendered content
33
+ return renderedContent;
34
+ }
35
+ // Props provided - re-render with merged context
36
+ try {
37
+ const mergedContext = {
38
+ ...baseContext,
39
+ props, // Make props available as stati.props
40
+ };
41
+ // Render the partial with the merged context using renderAsync
42
+ // This is a synchronous call despite the name when used with already-loaded templates
43
+ const result = eta.render(partialPath, mergedContext);
44
+ return result || '';
45
+ }
46
+ catch (error) {
47
+ console.error(`Error rendering callable partial ${partialPath} with props:`, error);
48
+ return `<!-- Error rendering partial with props: ${error instanceof Error ? error.message : String(error)} -->`;
49
+ }
50
+ };
51
+ // Create a Proxy to handle different usage patterns
52
+ const proxy = new Proxy(callable, {
53
+ /**
54
+ * Handle function calls: stati.partials.header({ props })
55
+ */
56
+ apply(target, thisArg, args) {
57
+ return target.apply(thisArg, args);
58
+ },
59
+ /**
60
+ * Handle toString(): When used in template interpolation without parentheses
61
+ * Example: <%~ stati.partials.header %>
62
+ */
63
+ get(target, prop) {
64
+ if (prop === 'toString' || prop === 'valueOf') {
65
+ return () => renderedContent;
66
+ }
67
+ // Allow other function properties to pass through
68
+ return Reflect.get(target, prop);
69
+ },
70
+ });
71
+ return proxy;
72
+ }
73
+ /**
74
+ * Wraps all partials in a record with callable partial wrappers.
75
+ * This allows partials to be used both as values and as functions.
76
+ *
77
+ * @param eta - The Eta template engine instance
78
+ * @param partials - Record mapping partial names to their rendered content
79
+ * @param partialPaths - Record mapping partial names to their absolute file paths
80
+ * @param baseContext - The base template context (without props)
81
+ * @returns Record of callable partials
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const callablePartials = wrapPartialsAsCallable(
86
+ * eta,
87
+ * { header: '<div>Header</div>', footer: '<div>Footer</div>' },
88
+ * { header: '/path/to/header.eta', footer: '/path/to/footer.eta' },
89
+ * baseContext
90
+ * );
91
+ *
92
+ * // Both syntaxes work
93
+ * callablePartials.header.toString(); // Direct usage
94
+ * callablePartials.header({ title: 'Custom' }); // With props
95
+ * ```
96
+ */
97
+ export function wrapPartialsAsCallable(eta, partials, partialPaths, baseContext) {
98
+ const callablePartials = {};
99
+ for (const [name, renderedContent] of Object.entries(partials)) {
100
+ const partialPath = partialPaths[name];
101
+ if (!partialPath) {
102
+ console.warn(`No path found for partial "${name}", skipping callable wrapper`);
103
+ continue;
104
+ }
105
+ callablePartials[name] = makeCallablePartial(eta, partialPath, baseContext, renderedContent);
106
+ }
107
+ return callablePartials;
108
+ }
@@ -6,8 +6,12 @@ export { readFile, writeFile, pathExists, ensureDir, remove, copyFile, readdir,
6
6
  export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolveDevPaths, normalizeTemplatePath, resolveSrcPath, resolveOutPath, resolveStaticPath, } from './paths.js';
7
7
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.js';
8
8
  export { propValue } from './template-utils.js';
9
+ export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.js';
9
10
  export { createValidatingPartialsProxy } from './partial-validation.js';
11
+ export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
12
+ export type { CallablePartial } from './callable-partials.js';
10
13
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
14
+ export { createNavigationHelpers } from './navigation-helpers.js';
11
15
  export { resolvePrettyUrl } from './server.js';
12
16
  export type { PrettyUrlResult } from './server.js';
13
17
  export { createErrorOverlay, parseErrorDetails } from './error-overlay.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,SAAS,CAAC;AAGjB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAGxE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,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,SAAS,CAAC;AAGjB,OAAO,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGhD,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,yBAAyB,CAAC;AAGjC,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAGxE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
@@ -10,10 +10,16 @@ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolv
10
10
  export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.js';
11
11
  // Template utilities
12
12
  export { propValue } from './template-utils.js';
13
+ // Tailwind inventory utilities
14
+ export { trackTailwindClass, enableInventoryTracking, disableInventoryTracking, clearInventory, getInventory, getInventorySize, isTrackingEnabled, writeTailwindClassInventory, isTailwindUsed, resetTailwindDetection, loadPreviousInventory, } from './tailwind-inventory.js';
13
15
  // Partial validation utilities
14
16
  export { createValidatingPartialsProxy } from './partial-validation.js';
17
+ // Callable partial utilities
18
+ export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
15
19
  // Template error utilities
16
20
  export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
21
+ // Navigation helper utilities
22
+ export { createNavigationHelpers } from './navigation-helpers.js';
17
23
  // Server utilities
18
24
  export { resolvePrettyUrl } from './server.js';
19
25
  // Error overlay utilities
@@ -0,0 +1,124 @@
1
+ import type { NavNode, PageModel } from '../../types/index.js';
2
+ /**
3
+ * Navigation helper utilities for template context.
4
+ * Provides methods for querying and traversing the navigation tree.
5
+ */
6
+ /**
7
+ * Finds a navigation node by its path or URL.
8
+ *
9
+ * @param tree - Navigation tree to search
10
+ * @param path - Path or URL to find
11
+ * @returns The found node or undefined
12
+ */
13
+ export declare function findNode(tree: NavNode[], path: string): NavNode | undefined;
14
+ /**
15
+ * Gets the children of a specific navigation node by path.
16
+ *
17
+ * @param tree - Navigation tree to search
18
+ * @param path - Path of the parent node
19
+ * @returns Array of child nodes or empty array if not found or no children
20
+ */
21
+ export declare function getChildren(tree: NavNode[], path: string): NavNode[];
22
+ /**
23
+ * Gets the parent node of a specific path.
24
+ *
25
+ * @param tree - Navigation tree to search
26
+ * @param path - Path to find parent for
27
+ * @returns The parent node or undefined
28
+ */
29
+ export declare function getParent(tree: NavNode[], path: string): NavNode | undefined;
30
+ /**
31
+ * Gets the siblings of a specific path (nodes at the same level).
32
+ *
33
+ * @param tree - Navigation tree to search
34
+ * @param path - Path to find siblings for
35
+ * @param includeSelf - Whether to include the node itself in the results
36
+ * @returns Array of sibling nodes or empty array
37
+ */
38
+ export declare function getSiblings(tree: NavNode[], path: string, includeSelf?: boolean): NavNode[];
39
+ /**
40
+ * Gets a subtree starting from a specific path.
41
+ *
42
+ * @param tree - Navigation tree to search
43
+ * @param path - Root path for the subtree
44
+ * @returns The subtree as an array (single node with its children) or empty array
45
+ */
46
+ export declare function getSubtree(tree: NavNode[], path: string): NavNode[];
47
+ /**
48
+ * Gets the breadcrumb trail from root to a specific path.
49
+ *
50
+ * @param tree - Navigation tree to search
51
+ * @param path - Path to get breadcrumbs for
52
+ * @returns Array of nodes from root to the target path
53
+ */
54
+ export declare function getBreadcrumbs(tree: NavNode[], path: string): NavNode[];
55
+ /**
56
+ * Finds the current page's navigation node.
57
+ *
58
+ * @param tree - Navigation tree to search
59
+ * @param currentPage - Current page model
60
+ * @returns The navigation node for the current page or undefined
61
+ */
62
+ export declare function getCurrentNode(tree: NavNode[], currentPage: PageModel): NavNode | undefined;
63
+ /**
64
+ * Creates a navigation helpers object for template context.
65
+ *
66
+ * @param tree - The full navigation tree
67
+ * @param currentPage - The current page being rendered
68
+ * @returns Object with navigation helper methods
69
+ */
70
+ export declare function createNavigationHelpers(tree: NavNode[], currentPage: PageModel): {
71
+ /**
72
+ * The full navigation tree.
73
+ * Use stati.nav.tree to access the global navigation.
74
+ */
75
+ tree: NavNode[];
76
+ /**
77
+ * Gets the full navigation tree.
78
+ * @returns The complete navigation tree
79
+ */
80
+ getTree: () => NavNode[];
81
+ /**
82
+ * Finds a navigation node by path or URL.
83
+ * @param path - The path or URL to find
84
+ * @returns The found node or undefined
85
+ */
86
+ findNode: (path: string) => NavNode | undefined;
87
+ /**
88
+ * Gets the children of a navigation node.
89
+ * @param path - The path of the parent node
90
+ * @returns Array of child navigation nodes
91
+ */
92
+ getChildren: (path: string) => NavNode[];
93
+ /**
94
+ * Gets the parent of a navigation node.
95
+ * @param path - The path to find the parent for (defaults to current page)
96
+ * @returns The parent node or undefined
97
+ */
98
+ getParent: (path?: string) => NavNode | undefined;
99
+ /**
100
+ * Gets the siblings of a navigation node.
101
+ * @param path - The path to find siblings for (defaults to current page)
102
+ * @param includeSelf - Whether to include the node itself
103
+ * @returns Array of sibling nodes
104
+ */
105
+ getSiblings: (path?: string, includeSelf?: boolean) => NavNode[];
106
+ /**
107
+ * Gets a subtree starting from a specific path.
108
+ * @param path - The root path for the subtree
109
+ * @returns Array containing the subtree
110
+ */
111
+ getSubtree: (path: string) => NavNode[];
112
+ /**
113
+ * Gets the breadcrumb trail for a path.
114
+ * @param path - The path to get breadcrumbs for (defaults to current page)
115
+ * @returns Array of nodes from root to the target
116
+ */
117
+ getBreadcrumbs: (path?: string) => NavNode[];
118
+ /**
119
+ * Gets the current page's navigation node.
120
+ * @returns The navigation node for the current page or undefined
121
+ */
122
+ getCurrentNode: () => NavNode | undefined;
123
+ };
124
+ //# sourceMappingURL=navigation-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation-helpers.d.ts","sourceRoot":"","sources":["../../../src/core/utils/navigation-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;GAGG;AAEH;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAiB3E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAGpE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE5E;AAiCD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,EAAE,CAiBzF;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAGnE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAIvE;AAkCD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAE3F;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,SAAS;IAE3E;;;OAGG;;IAGH;;;OAGG;;IAGH;;;;OAIG;qBACc,MAAM;IAEvB;;;;OAIG;wBACiB,MAAM;IAE1B;;;;OAIG;uBACgB,MAAM;IAEzB;;;;;OAKG;yBACkB,MAAM;IAG3B;;;;OAIG;uBACgB,MAAM;IAEzB;;;;OAIG;4BACqB,MAAM;IAE9B;;;OAGG;;EAGN"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Navigation helper utilities for template context.
3
+ * Provides methods for querying and traversing the navigation tree.
4
+ */
5
+ /**
6
+ * Finds a navigation node by its path or URL.
7
+ *
8
+ * @param tree - Navigation tree to search
9
+ * @param path - Path or URL to find
10
+ * @returns The found node or undefined
11
+ */
12
+ export function findNode(tree, path) {
13
+ for (const node of tree) {
14
+ // Check current node
15
+ if (node.path === path || node.url === path) {
16
+ return node;
17
+ }
18
+ // Recursively search children
19
+ if (node.children && node.children.length > 0) {
20
+ const found = findNode(node.children, path);
21
+ if (found) {
22
+ return found;
23
+ }
24
+ }
25
+ }
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Gets the children of a specific navigation node by path.
30
+ *
31
+ * @param tree - Navigation tree to search
32
+ * @param path - Path of the parent node
33
+ * @returns Array of child nodes or empty array if not found or no children
34
+ */
35
+ export function getChildren(tree, path) {
36
+ const node = findNode(tree, path);
37
+ return node?.children || [];
38
+ }
39
+ /**
40
+ * Gets the parent node of a specific path.
41
+ *
42
+ * @param tree - Navigation tree to search
43
+ * @param path - Path to find parent for
44
+ * @returns The parent node or undefined
45
+ */
46
+ export function getParent(tree, path) {
47
+ return findParentNode(tree, path, null);
48
+ }
49
+ /**
50
+ * Helper function to find parent node recursively.
51
+ *
52
+ * @param nodes - Current nodes to search
53
+ * @param targetPath - Path we're looking for
54
+ * @param parent - Current parent node
55
+ * @returns The parent node or undefined
56
+ */
57
+ function findParentNode(nodes, targetPath, parent) {
58
+ for (const node of nodes) {
59
+ // Check if this node is the target
60
+ if (node.path === targetPath || node.url === targetPath) {
61
+ return parent || undefined;
62
+ }
63
+ // Recursively search children with this node as parent
64
+ if (node.children && node.children.length > 0) {
65
+ const found = findParentNode(node.children, targetPath, node);
66
+ if (found !== undefined) {
67
+ return found;
68
+ }
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Gets the siblings of a specific path (nodes at the same level).
75
+ *
76
+ * @param tree - Navigation tree to search
77
+ * @param path - Path to find siblings for
78
+ * @param includeSelf - Whether to include the node itself in the results
79
+ * @returns Array of sibling nodes or empty array
80
+ */
81
+ export function getSiblings(tree, path, includeSelf = false) {
82
+ const parent = getParent(tree, path);
83
+ // If no parent, check if the node is at root level
84
+ if (!parent) {
85
+ // Node might be at root level
86
+ const isAtRoot = tree.some((node) => node.path === path || node.url === path);
87
+ if (isAtRoot) {
88
+ return includeSelf ? tree : tree.filter((node) => node.path !== path && node.url !== path);
89
+ }
90
+ return [];
91
+ }
92
+ const siblings = parent.children || [];
93
+ return includeSelf
94
+ ? siblings
95
+ : siblings.filter((node) => node.path !== path && node.url !== path);
96
+ }
97
+ /**
98
+ * Gets a subtree starting from a specific path.
99
+ *
100
+ * @param tree - Navigation tree to search
101
+ * @param path - Root path for the subtree
102
+ * @returns The subtree as an array (single node with its children) or empty array
103
+ */
104
+ export function getSubtree(tree, path) {
105
+ const node = findNode(tree, path);
106
+ return node ? [node] : [];
107
+ }
108
+ /**
109
+ * Gets the breadcrumb trail from root to a specific path.
110
+ *
111
+ * @param tree - Navigation tree to search
112
+ * @param path - Path to get breadcrumbs for
113
+ * @returns Array of nodes from root to the target path
114
+ */
115
+ export function getBreadcrumbs(tree, path) {
116
+ const trail = [];
117
+ findBreadcrumbTrail(tree, path, trail);
118
+ return trail;
119
+ }
120
+ /**
121
+ * Helper function to build breadcrumb trail recursively.
122
+ *
123
+ * @param nodes - Current nodes to search
124
+ * @param targetPath - Path we're looking for
125
+ * @param trail - Current breadcrumb trail
126
+ * @returns True if the target was found in this branch
127
+ */
128
+ function findBreadcrumbTrail(nodes, targetPath, trail) {
129
+ for (const node of nodes) {
130
+ // Add current node to trail
131
+ trail.push(node);
132
+ // Check if this is the target
133
+ if (node.path === targetPath || node.url === targetPath) {
134
+ return true;
135
+ }
136
+ // Check children
137
+ if (node.children && node.children.length > 0) {
138
+ if (findBreadcrumbTrail(node.children, targetPath, trail)) {
139
+ return true;
140
+ }
141
+ }
142
+ // This branch didn't contain the target, remove this node from trail
143
+ trail.pop();
144
+ }
145
+ return false;
146
+ }
147
+ /**
148
+ * Finds the current page's navigation node.
149
+ *
150
+ * @param tree - Navigation tree to search
151
+ * @param currentPage - Current page model
152
+ * @returns The navigation node for the current page or undefined
153
+ */
154
+ export function getCurrentNode(tree, currentPage) {
155
+ return findNode(tree, currentPage.url);
156
+ }
157
+ /**
158
+ * Creates a navigation helpers object for template context.
159
+ *
160
+ * @param tree - The full navigation tree
161
+ * @param currentPage - The current page being rendered
162
+ * @returns Object with navigation helper methods
163
+ */
164
+ export function createNavigationHelpers(tree, currentPage) {
165
+ return {
166
+ /**
167
+ * The full navigation tree.
168
+ * Use stati.nav.tree to access the global navigation.
169
+ */
170
+ tree,
171
+ /**
172
+ * Gets the full navigation tree.
173
+ * @returns The complete navigation tree
174
+ */
175
+ getTree: () => tree,
176
+ /**
177
+ * Finds a navigation node by path or URL.
178
+ * @param path - The path or URL to find
179
+ * @returns The found node or undefined
180
+ */
181
+ findNode: (path) => findNode(tree, path),
182
+ /**
183
+ * Gets the children of a navigation node.
184
+ * @param path - The path of the parent node
185
+ * @returns Array of child navigation nodes
186
+ */
187
+ getChildren: (path) => getChildren(tree, path),
188
+ /**
189
+ * Gets the parent of a navigation node.
190
+ * @param path - The path to find the parent for (defaults to current page)
191
+ * @returns The parent node or undefined
192
+ */
193
+ getParent: (path) => getParent(tree, path || currentPage.url),
194
+ /**
195
+ * Gets the siblings of a navigation node.
196
+ * @param path - The path to find siblings for (defaults to current page)
197
+ * @param includeSelf - Whether to include the node itself
198
+ * @returns Array of sibling nodes
199
+ */
200
+ getSiblings: (path, includeSelf = false) => getSiblings(tree, path || currentPage.url, includeSelf),
201
+ /**
202
+ * Gets a subtree starting from a specific path.
203
+ * @param path - The root path for the subtree
204
+ * @returns Array containing the subtree
205
+ */
206
+ getSubtree: (path) => getSubtree(tree, path),
207
+ /**
208
+ * Gets the breadcrumb trail for a path.
209
+ * @param path - The path to get breadcrumbs for (defaults to current page)
210
+ * @returns Array of nodes from root to the target
211
+ */
212
+ getBreadcrumbs: (path) => getBreadcrumbs(tree, path || currentPage.url),
213
+ /**
214
+ * Gets the current page's navigation node.
215
+ * @returns The navigation node for the current page or undefined
216
+ */
217
+ getCurrentNode: () => getCurrentNode(tree, currentPage),
218
+ };
219
+ }
@@ -1,6 +1,9 @@
1
+ import { type CallablePartial } from './callable-partials.js';
1
2
  /**
2
3
  * Creates a development-mode Proxy for the partials object that throws errors
3
- * when accessing non-existent partials instead of returning undefined
4
+ * when accessing non-existent partials instead of returning undefined.
5
+ *
6
+ * Supports both string partials and CallablePartial.
4
7
  */
5
- export declare function createValidatingPartialsProxy(partials: Record<string, string>): Record<string, string>;
8
+ export declare function createValidatingPartialsProxy<T extends string | CallablePartial>(partials: Record<string, T>): Record<string, T>;
6
9
  //# sourceMappingURL=partial-validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"partial-validation.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.ts"],"names":[],"mappings":"AA8FA;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmDxB"}
1
+ {"version":3,"file":"partial-validation.d.ts","sourceRoot":"","sources":["../../../src/core/utils/partial-validation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA8F9D;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,SAAS,MAAM,GAAG,eAAe,EAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CA+EnB"}
@@ -84,7 +84,9 @@ function findSimilarPartialNames(targetName, availableNames) {
84
84
  }
85
85
  /**
86
86
  * Creates a development-mode Proxy for the partials object that throws errors
87
- * when accessing non-existent partials instead of returning undefined
87
+ * when accessing non-existent partials instead of returning undefined.
88
+ *
89
+ * Supports both string partials and CallablePartial.
88
90
  */
89
91
  export function createValidatingPartialsProxy(partials) {
90
92
  // In production, return partials as-is
@@ -92,6 +94,11 @@ export function createValidatingPartialsProxy(partials) {
92
94
  if (getEnv() === 'production') {
93
95
  return partials;
94
96
  }
97
+ // If there are no partials, return the empty object as-is
98
+ // This avoids proxy-related issues during test serialization
99
+ if (Object.keys(partials).length === 0) {
100
+ return partials;
101
+ }
95
102
  return new Proxy(partials, {
96
103
  get(target, prop, receiver) {
97
104
  // Allow normal object operations
@@ -104,18 +111,39 @@ export function createValidatingPartialsProxy(partials) {
104
111
  return target[propName];
105
112
  }
106
113
  // Special case: allow accessing length, toString, etc.
107
- if (propName in Object.prototype || propName === 'length') {
114
+ // Also handle test framework inspection properties
115
+ if (propName in Object.prototype ||
116
+ propName === 'length' ||
117
+ propName === 'constructor' ||
118
+ propName === 'then' || // Promise detection
119
+ propName === '$$typeof' || // React inspection
120
+ propName === 'nodeType' || // DOM node detection
121
+ propName === 'asymmetricMatch' || // Jest/Vitest matcher
122
+ propName === 'toJSON' // JSON serialization
123
+ ) {
108
124
  return Reflect.get(target, prop, receiver);
109
125
  }
110
126
  // Property doesn't exist - return error overlay HTML instead of throwing
111
127
  const availablePartials = Object.keys(target);
112
128
  const suggestions = findSimilarPartialNames(propName, availablePartials);
113
- // Special case: throw error if no partials are available at all
114
- if (availablePartials.length === 0) {
115
- throw new Error('No partials are available');
116
- }
117
129
  // In development, render an inline error overlay
118
- return createInlineErrorOverlay(propName, suggestions);
130
+ const errorHtml = createInlineErrorOverlay(propName, suggestions);
131
+ // Check if we're dealing with CallablePartials by testing a known partial
132
+ const samplePartial = Object.values(target)[0];
133
+ const isCallable = typeof samplePartial === 'function';
134
+ if (isCallable) {
135
+ // For CallablePartial, return a function that returns the error HTML
136
+ // This prevents "string is not a function" errors when templates call missing partials
137
+ // Accept any arguments to handle props being passed
138
+ const errorFunction = (..._args) => errorHtml;
139
+ errorFunction.toString = () => errorHtml;
140
+ errorFunction.valueOf = () => errorHtml;
141
+ return errorFunction;
142
+ }
143
+ else {
144
+ // For string partials, return the error HTML directly
145
+ return errorHtml;
146
+ }
119
147
  },
120
148
  has(target, prop) {
121
149
  return prop in target;
@@ -12,7 +12,7 @@ export interface PrettyUrlResult {
12
12
  * This handles common patterns like:
13
13
  * - /path/ -> /path/index.html
14
14
  * - /path/ -> /path.html (if no index.html exists)
15
- * - /path -> /path.html (when original path is not found)
15
+ * - /path -> /path.html (when original path is not found and has no extension)
16
16
  *
17
17
  * @param outDir The output directory to serve files from
18
18
  * @param requestPath The requested URL path
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/core/utils/server.ts"],"names":[],"mappings":"AAGA;;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,CA+C1B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/core/utils/server.ts"],"names":[],"mappings":"AAGA;;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"}
@@ -5,7 +5,7 @@ import { stat } from 'fs/promises';
5
5
  * This handles common patterns like:
6
6
  * - /path/ -> /path/index.html
7
7
  * - /path/ -> /path.html (if no index.html exists)
8
- * - /path -> /path.html (when original path is not found)
8
+ * - /path -> /path.html (when original path is not found and has no extension)
9
9
  *
10
10
  * @param outDir The output directory to serve files from
11
11
  * @param requestPath The requested URL path
@@ -55,6 +55,19 @@ export async function resolvePrettyUrl(outDir, requestPath, originalFilePath) {
55
55
  // Continue to not found
56
56
  }
57
57
  }
58
+ else if (!requestPath.includes('.')) {
59
+ // For requests without trailing slash and without extension, try .html
60
+ const htmlPath = join(outDir, `${requestPath}.html`);
61
+ try {
62
+ const stats = await stat(htmlPath);
63
+ if (stats.isFile()) {
64
+ return { filePath: htmlPath, found: true };
65
+ }
66
+ }
67
+ catch {
68
+ // Continue to not found
69
+ }
70
+ }
58
71
  // No fallback worked
59
72
  return { filePath: null, found: false };
60
73
  }