@stati/core 1.29.2 → 2.0.1
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/README.md +10 -13
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +77 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +39 -29
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +9 -7
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +156 -197
- package/dist/core/eta-plugin.d.ts +60 -0
- package/dist/core/eta-plugin.d.ts.map +1 -0
- package/dist/core/eta-plugin.js +143 -0
- package/dist/core/isg/builder.d.ts +37 -19
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +70 -153
- package/dist/core/isg/deps.d.ts +8 -70
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +10 -401
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +2 -1
- package/dist/core/isg/index.d.ts +2 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -3
- package/dist/core/isg/manifest.d.ts.map +1 -1
- package/dist/core/isg/manifest.js +6 -1
- package/dist/core/markdown.d.ts +1 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +3 -2
- package/dist/core/template-index.d.ts +209 -0
- package/dist/core/template-index.d.ts.map +1 -0
- package/dist/core/template-index.js +484 -0
- package/dist/core/template-render.d.ts +137 -0
- package/dist/core/template-render.d.ts.map +1 -0
- package/dist/core/template-render.js +444 -0
- package/dist/core/templates.d.ts +29 -11
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +115 -196
- package/dist/core/utils/context-validation.utils.d.ts +10 -6
- package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
- package/dist/core/utils/context-validation.utils.js +39 -16
- package/dist/core/utils/date.utils.d.ts +1 -1
- package/dist/core/utils/date.utils.js +1 -1
- package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
- package/dist/core/utils/dev-css-watcher.utils.js +3 -2
- package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
- package/dist/core/utils/error-overlay.utils.js +7 -45
- package/dist/core/utils/front-matter.utils.d.ts +50 -0
- package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
- package/dist/core/utils/front-matter.utils.js +157 -0
- package/dist/core/utils/index.d.ts +4 -5
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +5 -7
- package/dist/core/utils/paths.utils.d.ts +3 -3
- package/dist/core/utils/paths.utils.js +6 -6
- package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +15 -6
- package/dist/core/utils/template-discovery.utils.d.ts +3 -14
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +2 -62
- package/dist/core/utils/template-errors.utils.d.ts +14 -1
- package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
- package/dist/core/utils/template-errors.utils.js +45 -4
- package/dist/core/utils/theme.utils.d.ts +17 -0
- package/dist/core/utils/theme.utils.d.ts.map +1 -0
- package/dist/core/utils/theme.utils.js +59 -0
- package/dist/images/index.d.ts +1 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +0 -1
- package/dist/images/markdown-rule.d.ts +1 -2
- package/dist/images/markdown-rule.d.ts.map +1 -1
- package/dist/images/og.d.ts +2 -15
- package/dist/images/og.d.ts.map +1 -1
- package/dist/images/og.js +4 -47
- package/dist/images/runtime.d.ts +24 -5
- package/dist/images/runtime.d.ts.map +1 -1
- package/dist/images/runtime.js +14 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +4 -1
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +11 -8
- package/dist/metrics/types.d.ts +31 -5
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +2 -1
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.js +1115 -1136
- package/dist/types/config.d.ts +18 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +83 -16
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +48 -2
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/core/utils/callable-partials.utils.d.ts +0 -60
- package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
- package/dist/core/utils/callable-partials.utils.js +0 -127
- package/dist/core/utils/partial-validation.utils.d.ts +0 -9
- package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
- package/dist/core/utils/partial-validation.utils.js +0 -107
package/dist/core/dev.js
CHANGED
|
@@ -6,11 +6,12 @@ import { build } from './build.js';
|
|
|
6
6
|
import { invalidate } from './invalidate.js';
|
|
7
7
|
import { loadConfig } from '../config/loader.js';
|
|
8
8
|
import { clearTemplateEngineCache } from './templates.js';
|
|
9
|
+
import { startTemplateIndexSession, endTemplateIndexSession, getCachedTemplateIndex, toTemplateFile, } from './template-index.js';
|
|
9
10
|
import { clearMarkdownProcessorCache } from './markdown.js';
|
|
10
|
-
import { loadCacheManifest, saveCacheManifest, DevServerLockManager } from './isg/index.js';
|
|
11
|
-
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, resolveRequestFile, resolveNotFoundPage, getMimeType, parseRequestPath, createErrorOverlay, parseErrorDetails, TemplateError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, isEnabledEnvFlag, refreshCssOutputWatcher, normalizePathForComparison, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
|
|
11
|
+
import { loadCacheManifest, saveCacheManifest, hasRecordedTemplates, DevServerLockManager, } from './isg/index.js';
|
|
12
|
+
import { resolveDevPaths, resolveCacheDir, resolvePrettyUrl, resolveRequestFile, resolveNotFoundPage, getMimeType, parseRequestPath, createErrorOverlay, parseErrorDetails, TemplateError, FrontMatterError, createFallbackLogger, mergeServerOptions, createTypeScriptWatcher, isEnabledEnvFlag, refreshCssOutputWatcher, normalizePathForComparison, assertPortAvailable, toPortUnavailableError, } from './utils/index.js';
|
|
12
13
|
import { setEnv, getEnv } from '../env.js';
|
|
13
|
-
import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST,
|
|
14
|
+
import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, DEFAULT_OUT_DIR } from '../constants.js';
|
|
14
15
|
/**
|
|
15
16
|
* Loads and validates configuration for the dev server.
|
|
16
17
|
*/
|
|
@@ -67,6 +68,7 @@ async function performInitialBuild(configPath, logger, onError) {
|
|
|
67
68
|
* @param eventType - The type of change: 'add', 'change', or 'unlink'
|
|
68
69
|
* @param configPath - Optional path to the stati config file
|
|
69
70
|
* @param staticDir - The static assets directory path
|
|
71
|
+
* @param srcDir - The source directory path, whose `.eta` files are templates
|
|
70
72
|
* @param logger - Logger instance for output
|
|
71
73
|
* @param wsServer - WebSocket server for live reload notifications
|
|
72
74
|
* @param isBuildingRef - Mutable reference tracking whether a build is currently in progress
|
|
@@ -75,9 +77,11 @@ async function performInitialBuild(configPath, logger, onError) {
|
|
|
75
77
|
* ensuring each file is only processed once with its most recent event type.
|
|
76
78
|
* @param onError - Optional callback invoked when build errors occur
|
|
77
79
|
* @param onBuildEnd - Optional callback invoked after each build attempt finishes (success or failure)
|
|
80
|
+
* @param hasBuildError - Whether the last build failed, so a template change rebuilds even when
|
|
81
|
+
* no recorded page uses the template
|
|
78
82
|
* @param batchedChanges - Array of changes to process in a single batch (used for pending queue processing)
|
|
79
83
|
*/
|
|
80
|
-
async function performIncrementalRebuild(changedPath, eventType, configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, onBuildEnd, batchedChanges = []) {
|
|
84
|
+
async function performIncrementalRebuild(changedPath, eventType, configPath, staticDir, srcDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, onBuildEnd, hasBuildError, batchedChanges = []) {
|
|
81
85
|
// If a build is in progress, queue this change for processing after the current build
|
|
82
86
|
if (isBuildingRef.value) {
|
|
83
87
|
pendingChangesRef.changes.set(changedPath, eventType);
|
|
@@ -121,45 +125,51 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
121
125
|
const getRelativePath = (path) => path.replace(process.cwd(), '').replace(/\\/g, '/').replace(/^\//, '');
|
|
122
126
|
// Track template change result for better output
|
|
123
127
|
let templateChangeResult = null;
|
|
128
|
+
// Templates are .eta files in the source directory; other files are content or static assets
|
|
129
|
+
const isTemplate = (path) => toTemplateFile(srcDir, path) !== null;
|
|
130
|
+
const templateChanges = allChanges.filter((change) => isTemplate(change.path));
|
|
131
|
+
const otherChanges = allChanges.filter((change) => !isTemplate(change.path));
|
|
124
132
|
try {
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
templateChangeResult = await handleTemplateChange(changedPath, configPath, devLogger);
|
|
133
|
+
let rebuild = 'none';
|
|
134
|
+
if (templateChanges.length > 0) {
|
|
135
|
+
templateChangeResult = await markTemplateChanges(templateChanges, srcDir, hasBuildError?.() ?? false);
|
|
136
|
+
rebuild = templateChangeResult.rebuild;
|
|
130
137
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
// Markdown and static files: ISG decides what re-renders, such as the pages that list a
|
|
139
|
+
// changed page through a collection; the build also removes a deleted page's output
|
|
140
|
+
if (otherChanges.length > 0 && rebuild === 'none') {
|
|
141
|
+
rebuild = 'incremental';
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
// Static file changed - use normal rebuild
|
|
137
|
-
const devLogger = createDevLogger(false);
|
|
143
|
+
if (rebuild !== 'none') {
|
|
138
144
|
await build({
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// Progress output for template changes, which can rebuild many pages
|
|
146
|
+
logger: createDevLogger(templateChanges.length > 0),
|
|
147
|
+
force: rebuild === 'full',
|
|
141
148
|
clean: false,
|
|
149
|
+
// Template changes don't affect static assets
|
|
150
|
+
...(otherChanges.length === 0 && { skipAssetCopy: true }),
|
|
142
151
|
...(configPath && { configPath }),
|
|
143
152
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
153
|
+
// Clear any previous errors on successful build
|
|
154
|
+
if (onError) {
|
|
155
|
+
onError(null);
|
|
156
|
+
}
|
|
157
|
+
// Notify all connected clients to reload
|
|
158
|
+
if (wsServer) {
|
|
159
|
+
wsServer.clients.forEach((client) => {
|
|
160
|
+
const ws = client;
|
|
161
|
+
if (ws.readyState === 1) {
|
|
162
|
+
// WebSocket.OPEN
|
|
163
|
+
ws.send(JSON.stringify({ type: 'reload' }));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
158
167
|
}
|
|
159
168
|
const duration = Date.now() - startTime;
|
|
160
169
|
// Log rebuild result with affected page info for template changes
|
|
161
170
|
for (const change of allChanges) {
|
|
162
171
|
const relativePath = getRelativePath(change.path);
|
|
172
|
+
const template = templateChanges.includes(change);
|
|
163
173
|
let action;
|
|
164
174
|
if (change.eventType === 'unlink') {
|
|
165
175
|
action = 'deleted';
|
|
@@ -167,17 +177,30 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
167
177
|
else if (isStaticAsset(change.path)) {
|
|
168
178
|
action = 'copied';
|
|
169
179
|
}
|
|
180
|
+
else if (template) {
|
|
181
|
+
action = change.eventType === 'add' ? 'added' : 'changed';
|
|
182
|
+
}
|
|
170
183
|
else {
|
|
171
184
|
action = 'rebuilt';
|
|
172
185
|
}
|
|
186
|
+
logger.info?.(`▸ ${relativePath} ${action}`);
|
|
173
187
|
// For template changes, include affected page count
|
|
174
|
-
if (
|
|
188
|
+
if (template && templateChangeResult && rebuild !== 'none') {
|
|
175
189
|
const { affectedPages, totalPages } = templateChangeResult;
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
if (templateChangeResult.rebuild === 'full') {
|
|
191
|
+
logger.info?.(` All pages rebuilt in ${duration}ms`);
|
|
192
|
+
}
|
|
193
|
+
else if (affectedPages > 0) {
|
|
194
|
+
logger.info?.(` ${affectedPages}/${totalPages} pages rebuilt in ${duration}ms`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
logger.info?.(` Done in ${duration}ms`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (template) {
|
|
201
|
+
logger.info?.(change.eventType === 'unlink' ? ' No page used it' : ' No page uses it');
|
|
178
202
|
}
|
|
179
203
|
else {
|
|
180
|
-
logger.info?.(`▸ ${relativePath} ${action}`);
|
|
181
204
|
logger.info?.(` Done in ${duration}ms`);
|
|
182
205
|
}
|
|
183
206
|
}
|
|
@@ -206,127 +229,119 @@ async function performIncrementalRebuild(changedPath, eventType, configPath, sta
|
|
|
206
229
|
// Get all pending changes - the build will process all of them at once
|
|
207
230
|
const pendingChanges = Array.from(pendingChangesRef.changes.entries()).map(([path, evtType]) => ({ path, eventType: evtType }));
|
|
208
231
|
pendingChangesRef.changes.clear();
|
|
209
|
-
// Trigger another rebuild with all batched changes
|
|
210
|
-
//
|
|
232
|
+
// Trigger another rebuild with all batched changes: every template change in the batch
|
|
233
|
+
// updates the template index and marks its pages, then one build runs
|
|
211
234
|
const [first, ...remaining] = pendingChanges;
|
|
212
235
|
if (first) {
|
|
213
|
-
void performIncrementalRebuild(first.path, first.eventType, configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, onBuildEnd, remaining);
|
|
236
|
+
void performIncrementalRebuild(first.path, first.eventType, configPath, staticDir, srcDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, onBuildEnd, hasBuildError, remaining);
|
|
214
237
|
}
|
|
215
238
|
}
|
|
216
239
|
}
|
|
217
240
|
}
|
|
218
241
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
242
|
+
* Applies template changes before a rebuild. Pages record the templates they rendered and the
|
|
243
|
+
* names they looked up, so for each changed `.eta` file:
|
|
244
|
+
* - change: the pages that rendered it rebuild; when none did, no page uses it.
|
|
245
|
+
* - add: the template index learns the file, and the pages whose lookups now find it rebuild,
|
|
246
|
+
* such as the pages a new section override applies to.
|
|
247
|
+
* - unlink: the index forgets the file, and the pages that rendered or found it rebuild.
|
|
248
|
+
*
|
|
249
|
+
* Affected entries are marked stale in the saved manifest. Entries without recorded templates
|
|
250
|
+
* cannot say which pages use a file, so they get a forced full build.
|
|
222
251
|
*
|
|
223
|
-
* @
|
|
252
|
+
* @param lastBuildFailed - Whether the previous build failed; then a build runs even when no
|
|
253
|
+
* recorded page uses the file, so the error clears once the template that broke it is fixed
|
|
224
254
|
*/
|
|
225
|
-
async function
|
|
226
|
-
|
|
255
|
+
async function markTemplateChanges(changes, srcDir, lastBuildFailed) {
|
|
256
|
+
// Drop the cached Eta engine so the changed templates are recompiled on the next build.
|
|
257
|
+
// Markdown-only edits keep reusing it.
|
|
258
|
+
clearTemplateEngineCache();
|
|
259
|
+
// Keep the template index that dev builds reuse in step with the files
|
|
260
|
+
const index = getCachedTemplateIndex(srcDir);
|
|
261
|
+
for (const change of changes) {
|
|
262
|
+
const file = toTemplateFile(srcDir, change.path);
|
|
263
|
+
if (index && file) {
|
|
264
|
+
if (change.eventType === 'add') {
|
|
265
|
+
index.add(file);
|
|
266
|
+
}
|
|
267
|
+
else if (change.eventType === 'unlink') {
|
|
268
|
+
index.remove(file);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
227
272
|
try {
|
|
228
|
-
|
|
229
|
-
// are recompiled on the next build. This keeps rebuilds correct while still
|
|
230
|
-
// reusing the engine across the common markdown-only edit path.
|
|
231
|
-
clearTemplateEngineCache();
|
|
232
|
-
// Load existing cache manifest
|
|
273
|
+
const cacheDir = resolveCacheDir();
|
|
233
274
|
const cacheManifest = await loadCacheManifest(cacheDir);
|
|
234
275
|
if (!cacheManifest) {
|
|
235
|
-
|
|
236
|
-
await build({
|
|
237
|
-
logger,
|
|
238
|
-
force: false,
|
|
239
|
-
clean: false,
|
|
240
|
-
...(configPath && { configPath }),
|
|
241
|
-
});
|
|
242
|
-
return { affectedPages: 0, totalPages: 0 };
|
|
276
|
+
return { affectedPages: 0, totalPages: 0, rebuild: 'incremental' };
|
|
243
277
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
let
|
|
253
|
-
for (const [pagePath, entry] of
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
if (hasMatchingDep) {
|
|
262
|
-
affectedPagesCount++;
|
|
263
|
-
// Mark entry as stale by invalidating its inputsHash
|
|
264
|
-
// This forces a rebuild while preserving deps for fast update optimization
|
|
265
|
-
cacheManifest.entries[pagePath] = {
|
|
266
|
-
...entry,
|
|
267
|
-
inputsHash: 'STALE', // Forces rebuild in shouldRebuildPage
|
|
268
|
-
};
|
|
278
|
+
const entries = Object.entries(cacheManifest.entries);
|
|
279
|
+
const totalPages = entries.length;
|
|
280
|
+
if (entries.some(([, entry]) => !hasRecordedTemplates(entry))) {
|
|
281
|
+
return { affectedPages: totalPages, totalPages, rebuild: 'full' };
|
|
282
|
+
}
|
|
283
|
+
// Normalized for comparison only: watcher paths and manifest paths can differ in form
|
|
284
|
+
const changedFiles = new Set(changes.map((change) => normalizePathForComparison(change.path)));
|
|
285
|
+
const filesAddedOrRemoved = changes.some((change) => change.eventType !== 'change');
|
|
286
|
+
let affectedPages = 0;
|
|
287
|
+
for (const [pagePath, entry] of entries) {
|
|
288
|
+
const renderedChangedFile = entry.deps.some((dep) => changedFiles.has(normalizePathForComparison(dep)));
|
|
289
|
+
const lookupsChanged = filesAddedOrRemoved && index !== null && index.lookupsChanged(entry.templateLookups);
|
|
290
|
+
if (renderedChangedFile || lookupsChanged) {
|
|
291
|
+
affectedPages++;
|
|
292
|
+
// Forces a rebuild in shouldRebuildPage
|
|
293
|
+
cacheManifest.entries[pagePath] = { ...entry, inputsHash: 'STALE' };
|
|
269
294
|
}
|
|
270
295
|
}
|
|
271
|
-
if (
|
|
272
|
-
// Save updated cache manifest
|
|
296
|
+
if (affectedPages > 0) {
|
|
273
297
|
await saveCacheManifest(cacheDir, cacheManifest);
|
|
274
|
-
|
|
275
|
-
// skipManifestSave: true because we already saved the manifest above
|
|
276
|
-
// skipAssetCopy: true because template changes don't affect static assets
|
|
277
|
-
await build({
|
|
278
|
-
logger,
|
|
279
|
-
force: false,
|
|
280
|
-
clean: false,
|
|
281
|
-
skipManifestSave: true,
|
|
282
|
-
skipAssetCopy: true,
|
|
283
|
-
...(configPath && { configPath }),
|
|
284
|
-
});
|
|
285
|
-
return { affectedPages: affectedPagesCount, totalPages };
|
|
298
|
+
return { affectedPages, totalPages, rebuild: 'incremental' };
|
|
286
299
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
await build({
|
|
292
|
-
logger,
|
|
293
|
-
force: true,
|
|
294
|
-
clean: false,
|
|
295
|
-
...(configPath && { configPath }),
|
|
296
|
-
});
|
|
297
|
-
return { affectedPages: totalPages, totalPages };
|
|
300
|
+
// Before the session has an index, the build's own checks find the pages a new or deleted
|
|
301
|
+
// file affects
|
|
302
|
+
if ((filesAddedOrRemoved && index === null) || lastBuildFailed) {
|
|
303
|
+
return { affectedPages: 0, totalPages, rebuild: 'incremental' };
|
|
298
304
|
}
|
|
305
|
+
return { affectedPages: 0, totalPages, rebuild: 'none' };
|
|
299
306
|
}
|
|
300
|
-
catch
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
await build({
|
|
304
|
-
logger,
|
|
305
|
-
force: false,
|
|
306
|
-
clean: false,
|
|
307
|
-
...(configPath && { configPath }),
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
catch (fallbackError) {
|
|
311
|
-
throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError));
|
|
312
|
-
}
|
|
313
|
-
// Return zeros on error fallback
|
|
314
|
-
return { affectedPages: 0, totalPages: 0 };
|
|
307
|
+
catch {
|
|
308
|
+
// Let the build's own checks decide
|
|
309
|
+
return { affectedPages: 0, totalPages: 0, rebuild: 'incremental' };
|
|
315
310
|
}
|
|
316
311
|
}
|
|
317
312
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* and the pages that read the navigation when the change altered it. A deleted page's output
|
|
321
|
-
* is removed by the build.
|
|
313
|
+
* The error overlay's details for a failed build: template errors point at the template and
|
|
314
|
+
* front matter errors at the Markdown file; other errors are classified by their message.
|
|
322
315
|
*/
|
|
323
|
-
async function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
async function describeBuildError(error) {
|
|
317
|
+
if (error instanceof TemplateError) {
|
|
318
|
+
return error.toErrorDetails();
|
|
319
|
+
}
|
|
320
|
+
if (error instanceof FrontMatterError) {
|
|
321
|
+
// The file and line are the Markdown file's, not a stack frame's
|
|
322
|
+
const { line: _line, column: _column, ...details } = parseErrorDetails(error, 'markdown', error.filePath);
|
|
323
|
+
return {
|
|
324
|
+
...details,
|
|
325
|
+
...(error.line !== undefined && { line: error.line }),
|
|
326
|
+
...(error.column !== undefined && { column: error.column }),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const message = error.message.toLowerCase();
|
|
330
|
+
const name = error.name ? error.name.toLowerCase() : '';
|
|
331
|
+
const errorType = message.includes('template') ||
|
|
332
|
+
message.includes('eta') ||
|
|
333
|
+
message.includes('layout') ||
|
|
334
|
+
name.includes('template')
|
|
335
|
+
? 'template'
|
|
336
|
+
: message.includes('front-matter') ||
|
|
337
|
+
message.includes('yaml') ||
|
|
338
|
+
name.includes('yaml') ||
|
|
339
|
+
name.includes('yamlexception')
|
|
340
|
+
? 'markdown'
|
|
341
|
+
: message.includes('config') || name.includes('config')
|
|
342
|
+
? 'config'
|
|
343
|
+
: 'build';
|
|
344
|
+
return parseErrorDetails(error, errorType);
|
|
330
345
|
}
|
|
331
346
|
export async function createDevServer(options = {}) {
|
|
332
347
|
const logger = options.logger ?? createFallbackLogger();
|
|
@@ -356,6 +371,7 @@ export async function createDevServer(options = {}) {
|
|
|
356
371
|
const setLastBuildError = (error) => {
|
|
357
372
|
lastBuildError = error;
|
|
358
373
|
};
|
|
374
|
+
const hasLastBuildError = () => lastBuildError !== null;
|
|
359
375
|
let watcher = null;
|
|
360
376
|
let cssWatcher = null;
|
|
361
377
|
let tsWatchers = [];
|
|
@@ -405,31 +421,7 @@ export async function createDevServer(options = {}) {
|
|
|
405
421
|
// If there's a build error, show error overlay for HTML requests
|
|
406
422
|
if (errorToShow &&
|
|
407
423
|
(requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
|
|
408
|
-
|
|
409
|
-
// Use enhanced error details for TemplateError instances
|
|
410
|
-
if (errorToShow instanceof TemplateError) {
|
|
411
|
-
errorDetails = await errorToShow.toErrorDetails();
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
// Determine error type based on error message
|
|
415
|
-
const message = errorToShow.message.toLowerCase();
|
|
416
|
-
const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
|
|
417
|
-
const errorType = message.includes('template') ||
|
|
418
|
-
message.includes('eta') ||
|
|
419
|
-
message.includes('layout') ||
|
|
420
|
-
name.includes('template')
|
|
421
|
-
? 'template'
|
|
422
|
-
: message.includes('front-matter') ||
|
|
423
|
-
message.includes('yaml') ||
|
|
424
|
-
name.includes('yaml') ||
|
|
425
|
-
name.includes('yamlexception')
|
|
426
|
-
? 'markdown'
|
|
427
|
-
: message.includes('config') || name.includes('config')
|
|
428
|
-
? 'config'
|
|
429
|
-
: 'build';
|
|
430
|
-
errorDetails = parseErrorDetails(errorToShow, errorType);
|
|
431
|
-
}
|
|
432
|
-
const errorHtml = createErrorOverlay(errorDetails, requestPath);
|
|
424
|
+
const errorHtml = createErrorOverlay(await describeBuildError(errorToShow), requestPath);
|
|
433
425
|
return {
|
|
434
426
|
content: errorHtml,
|
|
435
427
|
mimeType: 'text/html',
|
|
@@ -448,41 +440,6 @@ export async function createDevServer(options = {}) {
|
|
|
448
440
|
// Use the shared pretty URL resolver
|
|
449
441
|
const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, requestFile);
|
|
450
442
|
if (!found || !filePath) {
|
|
451
|
-
// If we have a build error and this is an HTML request, show error overlay instead of 404
|
|
452
|
-
const errorToShow = lastBuildError;
|
|
453
|
-
if (errorToShow &&
|
|
454
|
-
(requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
|
|
455
|
-
// Use enhanced error details for TemplateError instances
|
|
456
|
-
let errorDetails;
|
|
457
|
-
if (errorToShow instanceof TemplateError) {
|
|
458
|
-
errorDetails = await errorToShow.toErrorDetails();
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
// Determine error type based on error message
|
|
462
|
-
const message = errorToShow.message.toLowerCase();
|
|
463
|
-
const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
|
|
464
|
-
const errorType = message.includes('template') ||
|
|
465
|
-
message.includes('eta') ||
|
|
466
|
-
message.includes('layout') ||
|
|
467
|
-
name.includes('template')
|
|
468
|
-
? 'template'
|
|
469
|
-
: message.includes('front-matter') ||
|
|
470
|
-
message.includes('yaml') ||
|
|
471
|
-
name.includes('yaml') ||
|
|
472
|
-
name.includes('yamlexception')
|
|
473
|
-
? 'markdown'
|
|
474
|
-
: message.includes('config') || name.includes('config')
|
|
475
|
-
? 'config'
|
|
476
|
-
: 'build';
|
|
477
|
-
errorDetails = parseErrorDetails(errorToShow, errorType);
|
|
478
|
-
}
|
|
479
|
-
const errorHtml = createErrorOverlay(errorDetails, requestPath);
|
|
480
|
-
return {
|
|
481
|
-
content: errorHtml,
|
|
482
|
-
mimeType: 'text/html',
|
|
483
|
-
statusCode: 500,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
443
|
// Serve the site's 404 page, as static hosts do, with live reload so it updates on edits
|
|
487
444
|
const notFoundPage = await resolveNotFoundPage(outDir);
|
|
488
445
|
if (notFoundPage) {
|
|
@@ -582,7 +539,8 @@ export async function createDevServer(options = {}) {
|
|
|
582
539
|
console.log();
|
|
583
540
|
}
|
|
584
541
|
}
|
|
585
|
-
//
|
|
542
|
+
// Rebuilds reuse one template index, which the file watcher keeps current
|
|
543
|
+
startTemplateIndexSession(srcDir);
|
|
586
544
|
await performInitialBuild(configPath, logger, setLastBuildError);
|
|
587
545
|
// Create HTTP server
|
|
588
546
|
httpServer = createServer(async (req, res) => {
|
|
@@ -687,13 +645,13 @@ export async function createDevServer(options = {}) {
|
|
|
687
645
|
}
|
|
688
646
|
}
|
|
689
647
|
watcher.on('change', (path) => {
|
|
690
|
-
void performIncrementalRebuild(path, 'change', configPath, staticDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher);
|
|
648
|
+
void performIncrementalRebuild(path, 'change', configPath, staticDir, srcDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher, hasLastBuildError);
|
|
691
649
|
});
|
|
692
650
|
watcher.on('add', (path) => {
|
|
693
|
-
void performIncrementalRebuild(path, 'add', configPath, staticDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher);
|
|
651
|
+
void performIncrementalRebuild(path, 'add', configPath, staticDir, srcDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher, hasLastBuildError);
|
|
694
652
|
});
|
|
695
653
|
watcher.on('unlink', (path) => {
|
|
696
|
-
void performIncrementalRebuild(path, 'unlink', configPath, staticDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher);
|
|
654
|
+
void performIncrementalRebuild(path, 'unlink', configPath, staticDir, srcDir, logger, disableWsReload ? null : wsServer, isBuildingRef, pendingChangesRef, setLastBuildError, disableCssWatcher ? undefined : refreshCssWatcher, hasLastBuildError);
|
|
697
655
|
});
|
|
698
656
|
logger.success?.(`Dev server running at ${url}`);
|
|
699
657
|
logger.info?.(`\nServing:`);
|
|
@@ -757,6 +715,7 @@ export async function createDevServer(options = {}) {
|
|
|
757
715
|
}
|
|
758
716
|
// Release cached dev-session engines so they can be garbage collected
|
|
759
717
|
clearTemplateEngineCache();
|
|
718
|
+
endTemplateIndexSession(srcDir);
|
|
760
719
|
clearMarkdownProcessorCache();
|
|
761
720
|
logger.info?.('Dev server stopped');
|
|
762
721
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stati's Eta plugin. Eta compiles every template with functions of its own: `include()`,
|
|
3
|
+
* `includeAsync()`, `layout()`, `block()` and `blockAsync()`. They resolve names their own way,
|
|
4
|
+
* skip validation and the template index, and a template reached through Eta's `layout()`
|
|
5
|
+
* inherits its child's `stati`. Stati templates compose with `stati.include()` and
|
|
6
|
+
* `stati.layout()`, so the plugin replaces Eta's functions in each compiled template with ones
|
|
7
|
+
* that report the Stati call to use.
|
|
8
|
+
* @module core/eta-plugin
|
|
9
|
+
*/
|
|
10
|
+
import type { Eta, EtaConfig } from 'eta';
|
|
11
|
+
type EtaPlugin = EtaConfig['plugins'][number];
|
|
12
|
+
/**
|
|
13
|
+
* Eta's declaration of its `layout()` function. The plugin checks for it: when Eta generates
|
|
14
|
+
* different code, creating the engine fails instead of templates silently getting Eta's
|
|
15
|
+
* functions back.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ETA_LAYOUT_DECLARATION = "function layout(path, data) {\n __eta.layout = path;\n __eta.layoutData = data;\n}";
|
|
18
|
+
/** Eta's template functions that Stati replaces, and the code that declares each one. */
|
|
19
|
+
declare const ETA_FUNCTIONS: readonly [{
|
|
20
|
+
readonly name: "include";
|
|
21
|
+
readonly declaration: "let include = ";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "includeAsync";
|
|
24
|
+
readonly declaration: "let includeAsync = ";
|
|
25
|
+
}, {
|
|
26
|
+
readonly name: "layout";
|
|
27
|
+
readonly declaration: "function layout(path, data) {\n __eta.layout = path;\n __eta.layoutData = data;\n}";
|
|
28
|
+
}, {
|
|
29
|
+
readonly name: "block";
|
|
30
|
+
readonly declaration: "function block(";
|
|
31
|
+
}, {
|
|
32
|
+
readonly name: "blockAsync";
|
|
33
|
+
readonly declaration: "async function blockAsync(";
|
|
34
|
+
}];
|
|
35
|
+
/** One of Eta's template functions that Stati blocks. */
|
|
36
|
+
export type BlockedEtaFunction = (typeof ETA_FUNCTIONS)[number]['name'];
|
|
37
|
+
/**
|
|
38
|
+
* Creates the plugin. The replacements go at the start of the template's code rather than
|
|
39
|
+
* next to Eta's declarations, because with `debug` on Eta declares `block()` inside a `try`
|
|
40
|
+
* block, where a replacement made outside it would be shadowed. The template's code then runs
|
|
41
|
+
* in a block of its own, so it can still declare names such as `block` or `output`, which
|
|
42
|
+
* would otherwise clash with Eta's declarations.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createStatiEtaPlugin(): EtaPlugin;
|
|
45
|
+
/**
|
|
46
|
+
* Installs the handler that the replaced functions call, and compiles an empty template so
|
|
47
|
+
* that an Eta version the plugin does not match fails here rather than on the first page.
|
|
48
|
+
*
|
|
49
|
+
* @throws {Error} When Eta's generated code lacks the declarations the plugin replaces
|
|
50
|
+
*/
|
|
51
|
+
export declare function installEtaPlugin(eta: Eta): void;
|
|
52
|
+
/**
|
|
53
|
+
* The message for a blocked call, and the replacement to suggest.
|
|
54
|
+
*/
|
|
55
|
+
export declare function describeBlockedCall(name: BlockedEtaFunction, args: readonly unknown[]): {
|
|
56
|
+
message: string;
|
|
57
|
+
suggestion: string;
|
|
58
|
+
};
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=eta-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eta-plugin.d.ts","sourceRoot":"","sources":["../../src/core/eta-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAK1C,KAAK,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG9C;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,yFACqD,CAAC;AAEzF,yFAAyF;AACzF,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;EAMT,CAAC;AAEX,yDAAyD;AACzD,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAUxE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,SAAS,CAgChD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAG/C;AAqBD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA8BzC"}
|