@stati/core 1.7.1 → 1.9.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.
- package/dist/core/build.js +2 -2
- package/dist/core/isg/deps.js +21 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +7 -3
- package/dist/core/utils/callable-partials.d.ts +60 -0
- package/dist/core/utils/callable-partials.d.ts.map +1 -0
- package/dist/core/utils/callable-partials.js +108 -0
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +2 -0
- package/dist/core/utils/partial-validation.d.ts +5 -2
- package/dist/core/utils/partial-validation.d.ts.map +1 -1
- package/dist/core/utils/partial-validation.js +35 -7
- package/dist/seo/auto-inject.js +2 -2
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +4 -4
- package/dist/seo/sitemap.d.ts.map +1 -1
- package/dist/seo/sitemap.js +10 -2
- package/package.json +1 -1
package/dist/core/build.js
CHANGED
|
@@ -352,8 +352,6 @@ async function buildInternal(options = {}) {
|
|
|
352
352
|
logger.building('Building your site...');
|
|
353
353
|
// Load configuration
|
|
354
354
|
const { config, outDir, cacheDir } = await loadAndValidateConfig(options);
|
|
355
|
-
// Load cache manifest for ISG
|
|
356
|
-
const { manifest } = await setupCacheAndManifest(cacheDir);
|
|
357
355
|
// Initialize cache stats
|
|
358
356
|
let cacheHits = 0;
|
|
359
357
|
let cacheMisses = 0;
|
|
@@ -364,6 +362,8 @@ async function buildInternal(options = {}) {
|
|
|
364
362
|
await remove(cacheDir);
|
|
365
363
|
}
|
|
366
364
|
await ensureDir(outDir);
|
|
365
|
+
// Load cache manifest for ISG (after potential clean operation)
|
|
366
|
+
const { manifest } = await setupCacheAndManifest(cacheDir);
|
|
367
367
|
// Load content and build navigation
|
|
368
368
|
console.log(); // Add spacing before content loading
|
|
369
369
|
const { pages, navigation, md, eta } = await loadContentAndBuildNavigation(config, options, logger);
|
package/dist/core/isg/deps.js
CHANGED
|
@@ -244,6 +244,27 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
}
|
|
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
|
|
253
|
+
];
|
|
254
|
+
for (const pattern of callablePartialPatterns) {
|
|
255
|
+
let match;
|
|
256
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
257
|
+
const partialName = match[1];
|
|
258
|
+
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
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
247
268
|
return dependencies;
|
|
248
269
|
}
|
|
249
270
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,mBAAmB,CAAC;AAwLzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAW7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAyKjB"}
|
package/dist/core/templates.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Eta } from 'eta';
|
|
|
2
2
|
import { join, dirname, relative, basename, posix } from 'path';
|
|
3
3
|
import glob from 'fast-glob';
|
|
4
4
|
import { TEMPLATE_EXTENSION } from '../constants.js';
|
|
5
|
-
import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, propValue, } from './utils/index.js';
|
|
5
|
+
import { getStatiVersion, isCollectionIndexPage, discoverLayout, getCollectionPathForPage, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, propValue, wrapPartialsAsCallable, } from './utils/index.js';
|
|
6
6
|
import { getEnv } from '../env.js';
|
|
7
7
|
import { generateSEO } from '../seo/index.js';
|
|
8
8
|
/**
|
|
@@ -223,9 +223,11 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
|
|
|
223
223
|
try {
|
|
224
224
|
// Create context with all previously rendered partials available
|
|
225
225
|
const combinedPartials = { ...renderedPartials, ...passRenderedPartials };
|
|
226
|
+
// Wrap partials as callable before passing to validation proxy
|
|
227
|
+
const callablePartials = wrapPartialsAsCallable(eta, combinedPartials, partialPaths, baseContext);
|
|
226
228
|
const partialContext = {
|
|
227
229
|
...baseContext,
|
|
228
|
-
partials: createValidatingPartialsProxy(
|
|
230
|
+
partials: createValidatingPartialsProxy(callablePartials), // Include both previous and current pass partials with validation
|
|
229
231
|
};
|
|
230
232
|
const renderedContent = await eta.renderAsync(partialPath, partialContext);
|
|
231
233
|
passRenderedPartials[partialName] = renderedContent;
|
|
@@ -268,9 +270,11 @@ export async function renderPage(page, body, config, eta, navigation, allPages)
|
|
|
268
270
|
break;
|
|
269
271
|
}
|
|
270
272
|
}
|
|
273
|
+
// Wrap final rendered partials as callable before passing to layout context
|
|
274
|
+
const callablePartials = wrapPartialsAsCallable(eta, renderedPartials, partialPaths, baseContext);
|
|
271
275
|
const context = {
|
|
272
276
|
...baseContext,
|
|
273
|
-
partials: createValidatingPartialsProxy(
|
|
277
|
+
partials: createValidatingPartialsProxy(callablePartials), // Add rendered partials with validation
|
|
274
278
|
};
|
|
275
279
|
try {
|
|
276
280
|
if (!layoutPath) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Eta } from 'eta';
|
|
2
|
+
/**
|
|
3
|
+
* Type definition for a callable partial function.
|
|
4
|
+
* Can be called with optional props or used directly as a value.
|
|
5
|
+
*/
|
|
6
|
+
export type CallablePartial = {
|
|
7
|
+
(props?: Record<string, unknown>): string;
|
|
8
|
+
toString(): string;
|
|
9
|
+
valueOf(): string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Creates a callable partial that can be used both as a value and as a function.
|
|
13
|
+
* This enables both syntaxes:
|
|
14
|
+
* - Direct usage: <%~ stati.partials.header %>
|
|
15
|
+
* - With props: <%~ stati.partials.hero({ title: 'Hello' }) %>
|
|
16
|
+
*
|
|
17
|
+
* @param eta - The Eta template engine instance
|
|
18
|
+
* @param partialPath - Absolute path to the partial template file
|
|
19
|
+
* @param baseContext - The base template context (without props)
|
|
20
|
+
* @param renderedContent - Pre-rendered content for the no-props case
|
|
21
|
+
* @returns A callable partial function
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const callable = makeCallablePartial(eta, '/path/to/partial.eta', baseContext, '<div>Header</div>');
|
|
26
|
+
*
|
|
27
|
+
* // Use without props (returns pre-rendered content)
|
|
28
|
+
* const html1 = callable.toString(); // '<div>Header</div>'
|
|
29
|
+
*
|
|
30
|
+
* // Use with props (re-renders with merged context)
|
|
31
|
+
* const html2 = callable({ title: 'Custom Title' }); // Renders with custom props
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function makeCallablePartial(eta: Eta, partialPath: string, baseContext: Record<string, unknown>, renderedContent: string): CallablePartial;
|
|
35
|
+
/**
|
|
36
|
+
* Wraps all partials in a record with callable partial wrappers.
|
|
37
|
+
* This allows partials to be used both as values and as functions.
|
|
38
|
+
*
|
|
39
|
+
* @param eta - The Eta template engine instance
|
|
40
|
+
* @param partials - Record mapping partial names to their rendered content
|
|
41
|
+
* @param partialPaths - Record mapping partial names to their absolute file paths
|
|
42
|
+
* @param baseContext - The base template context (without props)
|
|
43
|
+
* @returns Record of callable partials
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const callablePartials = wrapPartialsAsCallable(
|
|
48
|
+
* eta,
|
|
49
|
+
* { header: '<div>Header</div>', footer: '<div>Footer</div>' },
|
|
50
|
+
* { header: '/path/to/header.eta', footer: '/path/to/footer.eta' },
|
|
51
|
+
* baseContext
|
|
52
|
+
* );
|
|
53
|
+
*
|
|
54
|
+
* // Both syntaxes work
|
|
55
|
+
* callablePartials.header.toString(); // Direct usage
|
|
56
|
+
* callablePartials.header({ title: 'Custom' }); // With props
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function wrapPartialsAsCallable(eta: Eta, partials: Record<string, string>, partialPaths: Record<string, string>, baseContext: Record<string, unknown>): Record<string, CallablePartial>;
|
|
60
|
+
//# sourceMappingURL=callable-partials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callable-partials.d.ts","sourceRoot":"","sources":["../../../src/core/utils/callable-partials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAC1C,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,eAAe,EAAE,MAAM,GACtB,eAAe,CAqDjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAcjC"}
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,8 @@ export { resolveSrcDir, resolveOutDir, resolveStaticDir, resolveCacheDir, resolv
|
|
|
7
7
|
export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from './template-discovery.js';
|
|
8
8
|
export { propValue } from './template-utils.js';
|
|
9
9
|
export { createValidatingPartialsProxy } from './partial-validation.js';
|
|
10
|
+
export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
|
|
11
|
+
export type { CallablePartial } from './callable-partials.js';
|
|
10
12
|
export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
|
|
11
13
|
export { resolvePrettyUrl } from './server.js';
|
|
12
14
|
export type { PrettyUrlResult } from './server.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,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,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"}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { discoverLayout, isCollectionIndexPage, getCollectionPathForPage, } from
|
|
|
12
12
|
export { propValue } from './template-utils.js';
|
|
13
13
|
// Partial validation utilities
|
|
14
14
|
export { createValidatingPartialsProxy } from './partial-validation.js';
|
|
15
|
+
// Callable partial utilities
|
|
16
|
+
export { makeCallablePartial, wrapPartialsAsCallable } from './callable-partials.js';
|
|
15
17
|
// Template error utilities
|
|
16
18
|
export { TemplateError, parseEtaError, createTemplateError } from './template-errors.js';
|
|
17
19
|
// Server utilities
|
|
@@ -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,
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/seo/auto-inject.js
CHANGED
|
@@ -86,8 +86,8 @@ export function autoInjectSEO(html, options) {
|
|
|
86
86
|
// Inject SEO metadata before </head>
|
|
87
87
|
const before = html.substring(0, headClosePos);
|
|
88
88
|
const after = html.substring(headClosePos);
|
|
89
|
-
// Add proper indentation (
|
|
90
|
-
const injected = `${before}
|
|
89
|
+
// Add proper indentation (4 spaces) and newline
|
|
90
|
+
const injected = `${before} ${seoMetadata}\n${after}`;
|
|
91
91
|
logDebug(`Injected ${existingTags.size === 0 ? 'all' : 'missing'} SEO tags into ${page.url}`, {
|
|
92
92
|
debug,
|
|
93
93
|
config,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAS7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAsH3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAyE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAuDjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE;IACP,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,EACD,IAAI,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,GAChC,MAAM,CA6CR"}
|
package/dist/seo/generator.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generates meta tags, Open Graph tags, Twitter Cards, and structured data
|
|
4
4
|
*/
|
|
5
5
|
import { SEOTagType } from '../types/seo.js';
|
|
6
|
-
import { escapeHtml, validateSEOMetadata, generateRobotsContent } from './utils/index.js';
|
|
6
|
+
import { escapeHtml, validateSEOMetadata, generateRobotsContent, resolveAbsoluteUrl, } from './utils/index.js';
|
|
7
7
|
import { sanitizeStructuredData } from './utils/escape-and-validation.js';
|
|
8
8
|
/**
|
|
9
9
|
* Generate complete SEO metadata for a page.
|
|
@@ -94,7 +94,7 @@ export function generateSEOMetadata(ctx) {
|
|
|
94
94
|
}
|
|
95
95
|
// Canonical link
|
|
96
96
|
if (shouldGenerate(SEOTagType.Canonical)) {
|
|
97
|
-
const canonical = seo.canonical ||
|
|
97
|
+
const canonical = seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
|
|
98
98
|
meta.push(`<link rel="canonical" href="${escapeHtml(canonical)}">`);
|
|
99
99
|
}
|
|
100
100
|
// Robots meta tag
|
|
@@ -125,7 +125,7 @@ export function generateSEOMetadata(ctx) {
|
|
|
125
125
|
const sanitized = sanitizeStructuredData(seo.structuredData, logger);
|
|
126
126
|
meta.push(`<script type="application/ld+json">${JSON.stringify(sanitized)}</script>`);
|
|
127
127
|
}
|
|
128
|
-
return meta.join('\n
|
|
128
|
+
return meta.join('\n ');
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Generate Open Graph protocol meta tags.
|
|
@@ -148,7 +148,7 @@ export function generateOpenGraphTags(ctx) {
|
|
|
148
148
|
// Basic OG tags with fallback chain
|
|
149
149
|
const ogTitle = og.title || seo.title || page.frontMatter.title || config.site.title;
|
|
150
150
|
const ogDescription = og.description || seo.description || page.frontMatter.description;
|
|
151
|
-
const ogUrl = og.url || seo.canonical ||
|
|
151
|
+
const ogUrl = og.url || seo.canonical || resolveAbsoluteUrl(page.url || '/', siteUrl);
|
|
152
152
|
const ogType = og.type || 'website';
|
|
153
153
|
const ogSiteName = og.siteName || config.site.title;
|
|
154
154
|
tags.push(`<meta property="og:title" content="${escapeHtml(ogTitle)}">`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAqJtD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
|
package/dist/seo/sitemap.js
CHANGED
|
@@ -122,8 +122,16 @@ function determinePriority(page, rules, defaultPriority = 0.5) {
|
|
|
122
122
|
return validatePriority(rule.priority);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
-
else
|
|
126
|
-
|
|
125
|
+
else {
|
|
126
|
+
// For non-glob patterns, check exact match or path prefix
|
|
127
|
+
if (page.url === pattern) {
|
|
128
|
+
return validatePriority(rule.priority);
|
|
129
|
+
}
|
|
130
|
+
// For path prefix matching, ensure we match at path boundaries
|
|
131
|
+
// e.g., "/api" matches "/api/foo" but "/" only matches "/" exactly
|
|
132
|
+
if (pattern !== '/' && page.url.startsWith(pattern + '/')) {
|
|
133
|
+
return validatePriority(rule.priority);
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
return defaultPriority;
|