@stati/core 1.0.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.
@@ -0,0 +1,23 @@
1
+ import type { StatiConfig } from '../types.js';
2
+ /**
3
+ * Loads and validates Stati configuration from the project directory.
4
+ * Searches for configuration files in order: stati.config.ts, stati.config.js, stati.config.mjs
5
+ *
6
+ * @param cwd - Current working directory to search for config files
7
+ * @returns Promise resolving to the merged configuration object
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { loadConfig } from 'stati';
12
+ *
13
+ * // Load config from current directory
14
+ * const config = await loadConfig();
15
+ *
16
+ * // Load config from specific directory
17
+ * const config = await loadConfig('/path/to/project');
18
+ * ```
19
+ *
20
+ * @throws {Error} When configuration file exists but contains invalid JavaScript/TypeScript
21
+ */
22
+ export declare function loadConfig(cwd?: string): Promise<StatiConfig>;
23
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CAkClF"}
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ /**
5
+ * Default configuration values for Stati.
6
+ * Used as fallback when no configuration file is found.
7
+ */
8
+ const DEFAULT_CONFIG = {
9
+ srcDir: 'site',
10
+ outDir: 'dist',
11
+ staticDir: 'public',
12
+ site: {
13
+ title: 'My Site',
14
+ baseUrl: 'http://localhost:3000',
15
+ },
16
+ isg: {
17
+ enabled: true,
18
+ ttlSeconds: 3600,
19
+ maxAgeCapDays: 365,
20
+ },
21
+ };
22
+ /**
23
+ * Loads and validates Stati configuration from the project directory.
24
+ * Searches for configuration files in order: stati.config.ts, stati.config.js, stati.config.mjs
25
+ *
26
+ * @param cwd - Current working directory to search for config files
27
+ * @returns Promise resolving to the merged configuration object
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { loadConfig } from 'stati';
32
+ *
33
+ * // Load config from current directory
34
+ * const config = await loadConfig();
35
+ *
36
+ * // Load config from specific directory
37
+ * const config = await loadConfig('/path/to/project');
38
+ * ```
39
+ *
40
+ * @throws {Error} When configuration file exists but contains invalid JavaScript/TypeScript
41
+ */
42
+ export async function loadConfig(cwd = process.cwd()) {
43
+ const configPaths = [
44
+ join(cwd, 'stati.config.ts'),
45
+ join(cwd, 'stati.config.js'),
46
+ join(cwd, 'stati.config.mjs'),
47
+ ];
48
+ let configPath = null;
49
+ for (const path of configPaths) {
50
+ if (existsSync(path)) {
51
+ configPath = path;
52
+ break;
53
+ }
54
+ }
55
+ if (!configPath) {
56
+ return DEFAULT_CONFIG;
57
+ }
58
+ try {
59
+ const configUrl = pathToFileURL(resolve(configPath)).href;
60
+ const module = await import(configUrl);
61
+ const userConfig = module.default || module;
62
+ return {
63
+ ...DEFAULT_CONFIG,
64
+ ...userConfig,
65
+ site: { ...DEFAULT_CONFIG.site, ...userConfig.site },
66
+ isg: { ...DEFAULT_CONFIG.isg, ...userConfig.isg },
67
+ };
68
+ }
69
+ catch (error) {
70
+ console.error('Error loading config:', error);
71
+ return DEFAULT_CONFIG;
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ import type { BuildStats } from '../types.js';
2
+ /**
3
+ * Options for customizing the build process.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const options: BuildOptions = {
8
+ * force: true, // Force rebuild of all pages
9
+ * clean: true, // Clean output directory before build
10
+ * configPath: './custom.config.js', // Custom config file path
11
+ * includeDrafts: true // Include draft pages in build
12
+ * };
13
+ * ```
14
+ */
15
+ export interface BuildOptions {
16
+ /** Force rebuild of all pages, ignoring cache */
17
+ force?: boolean;
18
+ /** Clean the output directory before building */
19
+ clean?: boolean;
20
+ /** Path to a custom configuration file */
21
+ configPath?: string;
22
+ /** Include draft pages in the build */
23
+ includeDrafts?: boolean;
24
+ }
25
+ /**
26
+ * Builds the static site by processing content files and generating HTML pages.
27
+ * This is the main entry point for Stati's build process.
28
+ *
29
+ * @param options - Build configuration options
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { build } from 'stati';
34
+ *
35
+ * // Basic build
36
+ * await build();
37
+ *
38
+ * // Build with options
39
+ * await build({
40
+ * clean: true,
41
+ * force: true,
42
+ * configPath: './custom.config.js'
43
+ * });
44
+ * ```
45
+ *
46
+ * @throws {Error} When configuration loading fails
47
+ * @throws {Error} When content processing fails
48
+ * @throws {Error} When template rendering fails
49
+ */
50
+ export declare function build(options?: BuildOptions): Promise<BuildStats>;
51
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAoFD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAyG3E"}
@@ -0,0 +1,191 @@
1
+ import fse from 'fs-extra';
2
+ const { ensureDir, writeFile, copy, remove, pathExists, stat, readdir } = fse;
3
+ import { join, dirname } from 'path';
4
+ import { loadConfig } from '../config/loader.js';
5
+ import { loadContent } from './content.js';
6
+ import { createMarkdownProcessor, renderMarkdown } from './markdown.js';
7
+ import { createTemplateEngine, renderPage } from './templates.js';
8
+ import { buildNavigation } from './navigation.js';
9
+ /**
10
+ * Recursively calculates the total size of a directory in bytes.
11
+ * Used for build statistics.
12
+ *
13
+ * @param dirPath - Path to the directory
14
+ * @returns Total size in bytes
15
+ */
16
+ async function getDirectorySize(dirPath) {
17
+ if (!(await pathExists(dirPath))) {
18
+ return 0;
19
+ }
20
+ let totalSize = 0;
21
+ const items = await readdir(dirPath, { withFileTypes: true });
22
+ for (const item of items) {
23
+ const itemPath = join(dirPath, item.name);
24
+ if (item.isDirectory()) {
25
+ totalSize += await getDirectorySize(itemPath);
26
+ }
27
+ else {
28
+ const stats = await stat(itemPath);
29
+ totalSize += stats.size;
30
+ }
31
+ }
32
+ return totalSize;
33
+ }
34
+ /**
35
+ * Counts the number of files in a directory recursively.
36
+ * Used for build statistics.
37
+ *
38
+ * @param dirPath - Path to the directory
39
+ * @returns Total number of files
40
+ */
41
+ async function countFilesInDirectory(dirPath) {
42
+ if (!(await pathExists(dirPath))) {
43
+ return 0;
44
+ }
45
+ let fileCount = 0;
46
+ const items = await readdir(dirPath, { withFileTypes: true });
47
+ for (const item of items) {
48
+ const itemPath = join(dirPath, item.name);
49
+ if (item.isDirectory()) {
50
+ fileCount += await countFilesInDirectory(itemPath);
51
+ }
52
+ else {
53
+ fileCount++;
54
+ }
55
+ }
56
+ return fileCount;
57
+ }
58
+ /**
59
+ * Formats build statistics for display.
60
+ *
61
+ * @param stats - Build statistics to format
62
+ * @returns Formatted statistics string
63
+ */
64
+ function formatBuildStats(stats) {
65
+ const sizeKB = (stats.outputSizeBytes / 1024).toFixed(1);
66
+ const timeSeconds = (stats.buildTimeMs / 1000).toFixed(2);
67
+ let output = `📊 Build Statistics:
68
+ ⏱️ Build time: ${timeSeconds}s
69
+ 📄 Pages built: ${stats.totalPages}
70
+ 📦 Assets copied: ${stats.assetsCount}
71
+ 💾 Output size: ${sizeKB} KB`;
72
+ if (stats.cacheHits !== undefined && stats.cacheMisses !== undefined) {
73
+ const totalCacheRequests = stats.cacheHits + stats.cacheMisses;
74
+ const hitRate = totalCacheRequests > 0 ? ((stats.cacheHits / totalCacheRequests) * 100).toFixed(1) : '0';
75
+ output += `
76
+ 🎯 Cache hits: ${stats.cacheHits}/${totalCacheRequests} (${hitRate}%)`;
77
+ }
78
+ return output;
79
+ }
80
+ /**
81
+ * Builds the static site by processing content files and generating HTML pages.
82
+ * This is the main entry point for Stati's build process.
83
+ *
84
+ * @param options - Build configuration options
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * import { build } from 'stati';
89
+ *
90
+ * // Basic build
91
+ * await build();
92
+ *
93
+ * // Build with options
94
+ * await build({
95
+ * clean: true,
96
+ * force: true,
97
+ * configPath: './custom.config.js'
98
+ * });
99
+ * ```
100
+ *
101
+ * @throws {Error} When configuration loading fails
102
+ * @throws {Error} When content processing fails
103
+ * @throws {Error} When template rendering fails
104
+ */
105
+ export async function build(options = {}) {
106
+ const buildStartTime = Date.now();
107
+ console.log('🏗️ Building site...');
108
+ // Load configuration
109
+ const config = await loadConfig(options.configPath ? dirname(options.configPath) : process.cwd());
110
+ const outDir = join(process.cwd(), config.outDir);
111
+ // Create .stati cache directory
112
+ const cacheDir = join(process.cwd(), '.stati');
113
+ await ensureDir(cacheDir);
114
+ // Clean output directory if requested
115
+ if (options.clean) {
116
+ console.log('🧹 Cleaning output directory...');
117
+ await remove(outDir);
118
+ }
119
+ await ensureDir(outDir);
120
+ // Load all content
121
+ const pages = await loadContent(config, options.includeDrafts);
122
+ console.log(`📄 Found ${pages.length} pages`);
123
+ // Build navigation from pages
124
+ const navigation = buildNavigation(pages);
125
+ console.log(`🧭 Built navigation with ${navigation.length} top-level items`);
126
+ // Create processors
127
+ const md = await createMarkdownProcessor(config);
128
+ const eta = createTemplateEngine(config);
129
+ // Build context
130
+ const buildContext = { config, pages };
131
+ // Run beforeAll hook
132
+ if (config.hooks?.beforeAll) {
133
+ await config.hooks.beforeAll(buildContext);
134
+ }
135
+ // Render each page
136
+ for (const page of pages) {
137
+ console.log(` Building ${page.url}`);
138
+ // Run beforeRender hook
139
+ if (config.hooks?.beforeRender) {
140
+ await config.hooks.beforeRender({ page, config });
141
+ }
142
+ // Render markdown to HTML
143
+ const htmlContent = renderMarkdown(page.content, md);
144
+ // Render with template
145
+ const finalHtml = await renderPage(page, htmlContent, config, eta, navigation, pages);
146
+ // Determine output path - fix the logic here
147
+ let outputPath;
148
+ if (page.url === '/') {
149
+ outputPath = join(outDir, 'index.html');
150
+ }
151
+ else if (page.url.endsWith('/')) {
152
+ outputPath = join(outDir, page.url, 'index.html');
153
+ }
154
+ else {
155
+ outputPath = join(outDir, `${page.url}.html`);
156
+ }
157
+ // Ensure directory exists and write file
158
+ await ensureDir(dirname(outputPath));
159
+ await writeFile(outputPath, finalHtml, 'utf-8');
160
+ // Run afterRender hook
161
+ if (config.hooks?.afterRender) {
162
+ await config.hooks.afterRender({ page, config });
163
+ }
164
+ }
165
+ // Copy static assets and count them
166
+ let assetsCount = 0;
167
+ const staticDir = join(process.cwd(), config.staticDir);
168
+ if (await pathExists(staticDir)) {
169
+ await copy(staticDir, outDir, { overwrite: true });
170
+ assetsCount = await countFilesInDirectory(staticDir);
171
+ console.log(`📦 Copied ${assetsCount} static assets`);
172
+ }
173
+ // Run afterAll hook
174
+ if (config.hooks?.afterAll) {
175
+ await config.hooks.afterAll(buildContext);
176
+ }
177
+ // Calculate build statistics
178
+ const buildEndTime = Date.now();
179
+ const buildStats = {
180
+ totalPages: pages.length,
181
+ assetsCount,
182
+ buildTimeMs: buildEndTime - buildStartTime,
183
+ outputSizeBytes: await getDirectorySize(outDir),
184
+ // Cache stats would be populated here when caching is implemented
185
+ cacheHits: 0,
186
+ cacheMisses: 0,
187
+ };
188
+ console.log('✅ Build complete!');
189
+ console.log(formatBuildStats(buildStats));
190
+ return buildStats;
191
+ }
@@ -0,0 +1,19 @@
1
+ import type { PageModel, StatiConfig } from '../types.js';
2
+ /**
3
+ * Loads and parses all content files from the configured source directory.
4
+ *
5
+ * @param config - The STATI configuration object
6
+ * @param includeDrafts - Whether to include draft pages (marked with draft: true)
7
+ * @returns Array of parsed page models
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Load all content including drafts
12
+ * const pages = await loadContent(config, true);
13
+ *
14
+ * // Load only published content
15
+ * const publishedPages = await loadContent(config, false);
16
+ * ```
17
+ */
18
+ export declare function loadContent(config: StatiConfig, includeDrafts?: boolean): Promise<PageModel[]>;
19
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/core/content.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,OAAO,GACtB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyCtB"}
@@ -0,0 +1,68 @@
1
+ import glob from 'fast-glob';
2
+ import fse from 'fs-extra';
3
+ const { readFile } = fse;
4
+ import matter from 'gray-matter';
5
+ import { join, relative, dirname, basename } from 'path';
6
+ /**
7
+ * Loads and parses all content files from the configured source directory.
8
+ *
9
+ * @param config - The STATI configuration object
10
+ * @param includeDrafts - Whether to include draft pages (marked with draft: true)
11
+ * @returns Array of parsed page models
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Load all content including drafts
16
+ * const pages = await loadContent(config, true);
17
+ *
18
+ * // Load only published content
19
+ * const publishedPages = await loadContent(config, false);
20
+ * ```
21
+ */
22
+ export async function loadContent(config, includeDrafts) {
23
+ const contentDir = join(process.cwd(), config.srcDir);
24
+ // Exclude folders starting with underscore from content discovery
25
+ const files = await glob('**/*.md', {
26
+ cwd: contentDir,
27
+ absolute: true,
28
+ ignore: ['**/_*/**', '_*/**'],
29
+ });
30
+ const pages = [];
31
+ for (const file of files) {
32
+ const content = await readFile(file, 'utf-8');
33
+ const { data: frontMatter, content: markdown } = matter(content);
34
+ // Skip drafts unless explicitly included
35
+ if (frontMatter.draft && !includeDrafts) {
36
+ continue;
37
+ }
38
+ const relativePath = relative(contentDir, file);
39
+ const slug = computeSlug(relativePath);
40
+ const url = computeUrl(slug, frontMatter);
41
+ const page = {
42
+ slug,
43
+ url,
44
+ sourcePath: file,
45
+ frontMatter,
46
+ content: markdown,
47
+ };
48
+ if (frontMatter.publishedAt && typeof frontMatter.publishedAt === 'string') {
49
+ page.publishedAt = new Date(frontMatter.publishedAt);
50
+ }
51
+ pages.push(page);
52
+ }
53
+ return pages;
54
+ }
55
+ function computeSlug(relativePath) {
56
+ const dir = dirname(relativePath);
57
+ const name = basename(relativePath, '.md');
58
+ if (name === 'index') {
59
+ return dir === '.' ? '/' : `/${dir}`;
60
+ }
61
+ return dir === '.' ? `/${name}` : `/${dir}/${name}`;
62
+ }
63
+ function computeUrl(slug, frontMatter) {
64
+ if (frontMatter.permalink && typeof frontMatter.permalink === 'string') {
65
+ return frontMatter.permalink;
66
+ }
67
+ return slug.replace(/\/index$/, '/');
68
+ }
@@ -0,0 +1,2 @@
1
+ export declare function invalidate(query?: string): Promise<void>;
2
+ //# sourceMappingURL=invalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AAAA,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9D"}
@@ -0,0 +1,7 @@
1
+ export async function invalidate(query) {
2
+ // TODO This will be implemented in Milestone 4 (ISG)
3
+ console.log('Invalidate functionality will be available in a future release');
4
+ if (query) {
5
+ console.log(`Query: ${query}`);
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import type { StatiConfig } from '../types.js';
3
+ /**
4
+ * Creates and configures a MarkdownIt processor based on the provided configuration.
5
+ * Supports both plugin array format and configure function format.
6
+ */
7
+ export declare function createMarkdownProcessor(config: StatiConfig): Promise<MarkdownIt>;
8
+ export declare function renderMarkdown(content: string, md: MarkdownIt): string;
9
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,MAAM,CAEtE"}
@@ -0,0 +1,48 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ /**
3
+ * Creates and configures a MarkdownIt processor based on the provided configuration.
4
+ * Supports both plugin array format and configure function format.
5
+ */
6
+ export async function createMarkdownProcessor(config) {
7
+ const md = new MarkdownIt({
8
+ html: true,
9
+ linkify: true,
10
+ typographer: true,
11
+ });
12
+ // Apply plugins from array format
13
+ if (config.markdown?.plugins) {
14
+ for (const plugin of config.markdown.plugins) {
15
+ if (typeof plugin === 'string') {
16
+ // Plugin name only
17
+ try {
18
+ const pluginModule = await import(`markdown-it-${plugin}`);
19
+ const pluginFunction = pluginModule.default || pluginModule;
20
+ md.use(pluginFunction);
21
+ }
22
+ catch (error) {
23
+ console.warn(`Failed to load markdown plugin: markdown-it-${plugin}`, error);
24
+ }
25
+ }
26
+ else if (Array.isArray(plugin) && plugin.length >= 1) {
27
+ // Plugin name with options [name, options]
28
+ const [pluginName, options] = plugin;
29
+ try {
30
+ const pluginModule = await import(`markdown-it-${pluginName}`);
31
+ const pluginFunction = pluginModule.default || pluginModule;
32
+ md.use(pluginFunction, options);
33
+ }
34
+ catch (error) {
35
+ console.warn(`Failed to load markdown plugin: markdown-it-${pluginName}`, error);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ // Apply user configuration function (this runs after plugins for override capability)
41
+ if (config.markdown?.configure) {
42
+ config.markdown.configure(md);
43
+ }
44
+ return md;
45
+ }
46
+ export function renderMarkdown(content, md) {
47
+ return md.render(content);
48
+ }
@@ -0,0 +1,21 @@
1
+ import type { PageModel, NavNode } from '../types.js';
2
+ /**
3
+ * Builds a hierarchical navigation structure from pages.
4
+ * Groups pages by directory structure and sorts them appropriately.
5
+ *
6
+ * @param pages - Array of page models to build navigation from
7
+ * @returns Array of top-level navigation nodes
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const pages = [
12
+ * { url: '/blog/post-1', frontMatter: { title: 'Post 1', order: 2 } },
13
+ * { url: '/blog/post-2', frontMatter: { title: 'Post 2', order: 1 } },
14
+ * { url: '/about', frontMatter: { title: 'About' } }
15
+ * ];
16
+ * const nav = buildNavigation(pages);
17
+ * // Results in hierarchical structure with sorted blog posts
18
+ * ```
19
+ */
20
+ export declare function buildNavigation(pages: PageModel[]): NavNode[];
21
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,CAyC7D"}