@stati/core 1.3.2 → 1.5.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @stati/core
2
2
 
3
- The core engine for Stati, a lightweight TypeScript static site generator built with Vite-inspired architecture.
3
+ The core engine for Stati, a lightweight TypeScript static site generator built with modern architecture.
4
4
 
5
5
  ## Installation
6
6
 
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAQ7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAmLD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAqQxF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAa7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAoOD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2XxF"}
package/dist/core/dev.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { createServer } from 'http';
2
2
  import { join, extname } from 'path';
3
3
  import { posix } from 'path';
4
- import { readFile, stat } from 'fs/promises';
4
+ import { readFile } from 'fs/promises';
5
5
  import { WebSocketServer } from 'ws';
6
6
  import chokidar from 'chokidar';
7
7
  import { build } from './build.js';
8
+ import { invalidate } from './invalidate.js';
8
9
  import { loadConfig } from '../config/loader.js';
9
10
  import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
10
11
  import { resolveDevPaths, resolveCacheDir } from './utils/paths.js';
12
+ import { resolvePrettyUrl } from './utils/server.js';
13
+ import { createErrorOverlay, parseErrorDetails } from './utils/error-overlay.js';
14
+ import { TemplateError } from './utils/template-errors.js';
15
+ import { setEnv, getEnv } from '../env.js';
11
16
  import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION } from '../constants.js';
12
17
  /**
13
18
  * Loads and validates configuration for the dev server.
@@ -32,24 +37,35 @@ async function loadDevConfig(configPath, logger) {
32
37
  /**
33
38
  * Performs an initial build to ensure dist/ exists
34
39
  */
35
- async function performInitialBuild(configPath, logger) {
40
+ async function performInitialBuild(configPath, logger, onError) {
36
41
  try {
42
+ // Clear cache to ensure fresh build on dev server start
43
+ logger.info?.('Clearing cache for fresh development build...');
44
+ await invalidate();
37
45
  await build({
38
46
  logger,
39
47
  force: false,
40
48
  clean: false,
41
49
  ...(configPath && { configPath }),
42
50
  });
51
+ // Clear any previous errors on successful build
52
+ if (onError) {
53
+ onError(null);
54
+ }
43
55
  }
44
56
  catch (error) {
45
- logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
46
- throw error;
57
+ const buildError = error instanceof Error ? error : new Error(String(error));
58
+ logger.error?.(`Initial build failed: ${buildError.message}`);
59
+ // Store the error for display in browser - DON'T clear it even if partial rebuild succeeds
60
+ onError?.(buildError);
61
+ // In development, don't throw - let the dev server start and show errors in browser
62
+ logger.warning?.('Dev server will start with build errors. Check browser for error overlay.');
47
63
  }
48
64
  }
49
65
  /**
50
66
  * Performs incremental rebuild when files change, using ISG logic for smart rebuilds
51
67
  */
52
- async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef) {
68
+ async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef, onError) {
53
69
  if (isBuildingRef.value) {
54
70
  logger.info?.('Build in progress, skipping...');
55
71
  return;
@@ -84,6 +100,10 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
84
100
  ...(configPath && { configPath }),
85
101
  });
86
102
  }
103
+ // Clear any previous errors on successful build
104
+ if (onError) {
105
+ onError(null);
106
+ }
87
107
  // Notify all connected clients to reload
88
108
  if (wsServer) {
89
109
  wsServer.clients.forEach((client) => {
@@ -98,8 +118,13 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
98
118
  logger.info?.(`⚡ ${relativePath} rebuilt in ${duration}ms`);
99
119
  }
100
120
  catch (error) {
121
+ const buildError = error instanceof Error ? error : new Error(String(error));
101
122
  const duration = Date.now() - startTime;
102
- logger.error?.(`❌ Rebuild failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
123
+ logger.error?.(`❌ Rebuild failed after ${duration}ms: ${buildError.message}`);
124
+ // Store the error for display in browser
125
+ if (onError) {
126
+ onError(buildError);
127
+ }
103
128
  }
104
129
  finally {
105
130
  isBuildingRef.value = false;
@@ -125,8 +150,14 @@ async function handleTemplateChange(templatePath, configPath, logger) {
125
150
  }
126
151
  // Find pages that depend on this template
127
152
  const affectedPages = [];
153
+ const normalizedTemplatePath = posix.normalize(templatePath.replace(/\\/g, '/'));
128
154
  for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
129
- if (entry.deps.some((dep) => dep.includes(posix.normalize(templatePath.replace(/\\/g, '/'))))) {
155
+ if (entry.deps.some((dep) => {
156
+ const normalizedDep = posix.normalize(dep.replace(/\\/g, '/'));
157
+ // Use endsWith for more precise matching to avoid false positives
158
+ return (normalizedDep === normalizedTemplatePath ||
159
+ normalizedDep.endsWith('/' + normalizedTemplatePath));
160
+ })) {
130
161
  affectedPages.push(pagePath);
131
162
  // Remove from cache to force rebuild
132
163
  delete cacheManifest.entries[pagePath];
@@ -143,15 +174,31 @@ async function handleTemplateChange(templatePath, configPath, logger) {
143
174
  ...(configPath && { configPath }),
144
175
  });
145
176
  }
177
+ else {
178
+ // If no affected pages were found but a template changed,
179
+ // force a full rebuild to ensure changes are reflected
180
+ // This can happen if dependency tracking missed something
181
+ await build({
182
+ logger,
183
+ force: true,
184
+ clean: false,
185
+ ...(configPath && { configPath }),
186
+ });
187
+ }
146
188
  }
147
- catch {
148
- // Fallback to full rebuild
149
- await build({
150
- logger,
151
- force: false,
152
- clean: false,
153
- ...(configPath && { configPath }),
154
- });
189
+ catch (_error) {
190
+ try {
191
+ // Fallback to full rebuild
192
+ await build({
193
+ logger,
194
+ force: false,
195
+ clean: false,
196
+ ...(configPath && { configPath }),
197
+ });
198
+ }
199
+ catch (fallbackError) {
200
+ throw fallbackError instanceof Error ? fallbackError : new Error(String(fallbackError));
201
+ }
155
202
  }
156
203
  }
157
204
  export async function createDevServer(options = {}) {
@@ -164,9 +211,16 @@ export async function createDevServer(options = {}) {
164
211
  processing: (msg) => console.log(msg),
165
212
  stats: (msg) => console.log(msg),
166
213
  }, } = options;
214
+ setEnv('development');
167
215
  const url = `http://${host}:${port}`;
168
216
  let httpServer = null;
169
217
  let wsServer = null;
218
+ // Track build errors for display in browser
219
+ let lastBuildError = null;
220
+ // Function to set build errors for error overlay display
221
+ const setLastBuildError = (error) => {
222
+ lastBuildError = error;
223
+ };
170
224
  let watcher = null;
171
225
  const isBuildingRef = { value: false };
172
226
  // Load configuration
@@ -232,24 +286,90 @@ export async function createDevServer(options = {}) {
232
286
  * Serves files from the dist directory
233
287
  */
234
288
  async function serveFile(requestPath) {
235
- let filePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
236
- try {
237
- const stats = await stat(filePath);
238
- if (stats.isDirectory()) {
239
- // Try to serve index.html from directory
240
- const indexPath = join(filePath, 'index.html');
241
- try {
242
- await stat(indexPath);
243
- filePath = indexPath;
289
+ // Use the global build error
290
+ const errorToShow = lastBuildError;
291
+ // If there's a build error, show error overlay for HTML requests
292
+ if (errorToShow &&
293
+ (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
294
+ let errorDetails;
295
+ // Use enhanced error details for TemplateError instances
296
+ if (errorToShow instanceof TemplateError) {
297
+ errorDetails = await errorToShow.toErrorDetails();
298
+ }
299
+ else {
300
+ // Determine error type based on error message
301
+ const message = errorToShow.message.toLowerCase();
302
+ const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
303
+ const errorType = message.includes('template') ||
304
+ message.includes('eta') ||
305
+ message.includes('layout') ||
306
+ name.includes('template')
307
+ ? 'template'
308
+ : message.includes('front-matter') ||
309
+ message.includes('yaml') ||
310
+ name.includes('yaml') ||
311
+ name.includes('yamlexception')
312
+ ? 'markdown'
313
+ : message.includes('config') || name.includes('config')
314
+ ? 'config'
315
+ : 'build';
316
+ errorDetails = parseErrorDetails(errorToShow, errorType);
317
+ }
318
+ const errorHtml = createErrorOverlay(errorDetails, requestPath);
319
+ return {
320
+ content: errorHtml,
321
+ mimeType: 'text/html',
322
+ statusCode: 500,
323
+ };
324
+ }
325
+ const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
326
+ // Use the shared pretty URL resolver
327
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
328
+ if (!found || !filePath) {
329
+ // If we have a build error and this is an HTML request, show error overlay instead of 404
330
+ const errorToShow = lastBuildError;
331
+ if (errorToShow &&
332
+ (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.'))) {
333
+ // Use enhanced error details for TemplateError instances
334
+ let errorDetails;
335
+ if (errorToShow instanceof TemplateError) {
336
+ errorDetails = await errorToShow.toErrorDetails();
244
337
  }
245
- catch {
246
- return {
247
- content: '404 - Directory listing not available',
248
- mimeType: 'text/plain',
249
- statusCode: 404,
250
- };
338
+ else {
339
+ // Determine error type based on error message
340
+ const message = errorToShow.message.toLowerCase();
341
+ const name = errorToShow.name ? errorToShow.name.toLowerCase() : '';
342
+ const errorType = message.includes('template') ||
343
+ message.includes('eta') ||
344
+ message.includes('layout') ||
345
+ name.includes('template')
346
+ ? 'template'
347
+ : message.includes('front-matter') ||
348
+ message.includes('yaml') ||
349
+ name.includes('yaml') ||
350
+ name.includes('yamlexception')
351
+ ? 'markdown'
352
+ : message.includes('config') || name.includes('config')
353
+ ? 'config'
354
+ : 'build';
355
+ errorDetails = parseErrorDetails(errorToShow, errorType);
251
356
  }
357
+ const errorHtml = createErrorOverlay(errorDetails, requestPath);
358
+ return {
359
+ content: errorHtml,
360
+ mimeType: 'text/html',
361
+ statusCode: 500,
362
+ };
252
363
  }
364
+ return {
365
+ content: requestPath.endsWith('/')
366
+ ? '404 - Directory listing not available'
367
+ : '404 - File not found',
368
+ mimeType: 'text/plain',
369
+ statusCode: 404,
370
+ };
371
+ }
372
+ try {
253
373
  const mimeType = getMimeType(filePath);
254
374
  const content = await readFile(filePath);
255
375
  // Inject live reload script into HTML files
@@ -269,11 +389,11 @@ export async function createDevServer(options = {}) {
269
389
  };
270
390
  }
271
391
  catch {
272
- // File not found
392
+ // This should rarely happen since resolvePrettyUrl already checked the file exists
273
393
  return {
274
- content: '404 - File not found',
394
+ content: '500 - Error reading file',
275
395
  mimeType: 'text/plain',
276
- statusCode: 404,
396
+ statusCode: 500,
277
397
  };
278
398
  }
279
399
  }
@@ -281,7 +401,7 @@ export async function createDevServer(options = {}) {
281
401
  url,
282
402
  async start() {
283
403
  // Perform initial build
284
- await performInitialBuild(configPath, logger);
404
+ await performInitialBuild(configPath, logger, setLastBuildError);
285
405
  // Create HTTP server
286
406
  httpServer = createServer(async (req, res) => {
287
407
  const requestPath = req.url || '/';
@@ -306,17 +426,25 @@ export async function createDevServer(options = {}) {
306
426
  }
307
427
  });
308
428
  // Create WebSocket server for live reload
309
- wsServer = new WebSocketServer({
310
- server: httpServer,
311
- path: '/__ws',
312
- });
313
- wsServer.on('connection', (ws) => {
314
- logger.info?.('Browser connected for live reload');
315
- const websocket = ws;
316
- websocket.on('close', () => {
317
- logger.info?.('Browser disconnected from live reload');
318
- });
319
- });
429
+ if (getEnv() !== 'test') {
430
+ try {
431
+ wsServer = new WebSocketServer({
432
+ server: httpServer,
433
+ path: '/__ws',
434
+ });
435
+ wsServer.on('connection', (ws) => {
436
+ logger.info?.('Browser connected for live reload');
437
+ const websocket = ws;
438
+ websocket.on('close', () => {
439
+ logger.info?.('Browser disconnected from live reload');
440
+ });
441
+ });
442
+ }
443
+ catch (_error) {
444
+ logger.warning?.('WebSocket server creation failed, live reload will not be available');
445
+ wsServer = null;
446
+ }
447
+ }
320
448
  // Start HTTP server
321
449
  await new Promise((resolve, reject) => {
322
450
  httpServer.listen(port, host, () => {
@@ -334,13 +462,13 @@ export async function createDevServer(options = {}) {
334
462
  ignoreInitial: true,
335
463
  });
336
464
  watcher.on('change', (path) => {
337
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
465
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
338
466
  });
339
467
  watcher.on('add', (path) => {
340
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
468
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
341
469
  });
342
470
  watcher.on('unlink', (path) => {
343
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
471
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
344
472
  });
345
473
  logger.success?.(`Dev server running at ${url}`);
346
474
  logger.info?.(`\nServing from:`);
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAKhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnF"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiBhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnF"}
@@ -35,7 +35,19 @@ function createSha256Hash(data) {
35
35
  */
36
36
  export function computeContentHash(content, frontMatter) {
37
37
  // Hash the front matter (sorted for consistency)
38
- const sortedFrontMatter = JSON.stringify(frontMatter, Object.keys(frontMatter).sort());
38
+ // Note: We need to sort keys at all levels, not just top level
39
+ const sortKeys = (obj) => {
40
+ if (obj === null || typeof obj !== 'object')
41
+ return obj;
42
+ if (Array.isArray(obj))
43
+ return obj.map(sortKeys);
44
+ const sorted = {};
45
+ for (const key of Object.keys(obj).sort()) {
46
+ sorted[key] = sortKeys(obj[key]);
47
+ }
48
+ return sorted;
49
+ };
50
+ const sortedFrontMatter = JSON.stringify(sortKeys(frontMatter));
39
51
  return createSha256Hash([content, sortedFrontMatter]);
40
52
  }
41
53
  /**
@@ -1 +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,mBAAmB,CAAC;AAErD;;;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"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,aAAa,CAAC;AAIrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmBrD;;;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"}
@@ -1,4 +1,23 @@
1
1
  import MarkdownIt from 'markdown-it';
2
+ import { createRequire } from 'module';
3
+ import { pathToFileURL } from 'url';
4
+ import path from 'path';
5
+ /**
6
+ * Load a markdown plugin, trying different resolution strategies
7
+ */
8
+ async function loadMarkdownPlugin(pluginName) {
9
+ const fullPluginName = `markdown-it-${pluginName}`;
10
+ // Try importing from current working directory first (for projects using Stati)
11
+ try {
12
+ const require = createRequire(pathToFileURL(path.resolve(process.cwd(), 'package.json')));
13
+ const pluginPath = require.resolve(fullPluginName);
14
+ return await import(pathToFileURL(pluginPath).href);
15
+ }
16
+ catch {
17
+ // Fallback to standard resolution (for core package dependencies)
18
+ return await import(fullPluginName);
19
+ }
20
+ }
2
21
  /**
3
22
  * Creates and configures a MarkdownIt processor based on the provided configuration.
4
23
  * Supports both plugin array format and configure function format.
@@ -15,7 +34,7 @@ export async function createMarkdownProcessor(config) {
15
34
  if (typeof plugin === 'string') {
16
35
  // Plugin name only
17
36
  try {
18
- const pluginModule = await import(`markdown-it-${plugin}`);
37
+ const pluginModule = await loadMarkdownPlugin(plugin);
19
38
  const pluginFunction = pluginModule.default || pluginModule;
20
39
  md.use(pluginFunction);
21
40
  }
@@ -27,7 +46,7 @@ export async function createMarkdownProcessor(config) {
27
46
  // Plugin name with options [name, options]
28
47
  const [pluginName, options] = plugin;
29
48
  try {
30
- const pluginModule = await import(`markdown-it-${pluginName}`);
49
+ const pluginModule = await loadMarkdownPlugin(pluginName);
31
50
  const pluginFunction = pluginModule.default || pluginModule;
32
51
  md.use(pluginFunction, options);
33
52
  }
@@ -0,0 +1,19 @@
1
+ import type { Logger } from '../types/index.js';
2
+ export interface PreviewServerOptions {
3
+ port?: number;
4
+ host?: string;
5
+ open?: boolean;
6
+ configPath?: string;
7
+ logger?: Logger;
8
+ }
9
+ export interface PreviewServer {
10
+ start(): Promise<void>;
11
+ stop(): Promise<void>;
12
+ url: string;
13
+ }
14
+ /**
15
+ * Creates a preview server that serves the built site from the dist directory
16
+ * without live reload functionality, perfect for previewing the production build.
17
+ */
18
+ export declare function createPreviewServer(options?: PreviewServerOptions): Promise<PreviewServer>;
19
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/core/preview.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAMhD,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAyBD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CA2JxB"}
@@ -0,0 +1,159 @@
1
+ import { createServer } from 'http';
2
+ import { join, extname } from 'path';
3
+ import { readFile } from 'fs/promises';
4
+ import { loadConfig } from '../config/loader.js';
5
+ import { resolveDevPaths } from './utils/paths.js';
6
+ import { resolvePrettyUrl } from './utils/server.js';
7
+ import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST } from '../constants.js';
8
+ /**
9
+ * Loads and validates configuration for the preview server.
10
+ */
11
+ async function loadPreviewConfig(configPath, logger) {
12
+ try {
13
+ if (configPath) {
14
+ logger.info?.(`Loading config from: ${configPath}`);
15
+ }
16
+ const config = await loadConfig(process.cwd());
17
+ const { outDir } = resolveDevPaths(config);
18
+ return { outDir };
19
+ }
20
+ catch (error) {
21
+ logger.error?.(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
22
+ throw error;
23
+ }
24
+ }
25
+ /**
26
+ * Creates a preview server that serves the built site from the dist directory
27
+ * without live reload functionality, perfect for previewing the production build.
28
+ */
29
+ export async function createPreviewServer(options = {}) {
30
+ const { port = DEFAULT_DEV_PORT, host = DEFAULT_DEV_HOST, open = false, configPath, logger = {
31
+ info: () => { },
32
+ success: () => { },
33
+ error: (msg) => console.error(msg),
34
+ warning: (msg) => console.warn(msg),
35
+ building: () => { },
36
+ processing: () => { },
37
+ stats: () => { },
38
+ }, } = options;
39
+ const url = `http://${host}:${port}`;
40
+ let httpServer = null;
41
+ // Load configuration
42
+ const { outDir } = await loadPreviewConfig(configPath, logger);
43
+ /**
44
+ * Gets MIME type for a file based on its extension
45
+ */
46
+ function getMimeType(filePath) {
47
+ const ext = extname(filePath).toLowerCase();
48
+ const mimeTypes = {
49
+ '.html': 'text/html',
50
+ '.js': 'application/javascript',
51
+ '.css': 'text/css',
52
+ '.json': 'application/json',
53
+ '.png': 'image/png',
54
+ '.jpg': 'image/jpeg',
55
+ '.jpeg': 'image/jpeg',
56
+ '.gif': 'image/gif',
57
+ '.svg': 'image/svg+xml',
58
+ '.ico': 'image/x-icon',
59
+ '.webp': 'image/webp',
60
+ '.woff': 'font/woff',
61
+ '.woff2': 'font/woff2',
62
+ '.ttf': 'font/ttf',
63
+ '.eot': 'application/vnd.ms-fontobject',
64
+ };
65
+ return mimeTypes[ext] || 'application/octet-stream';
66
+ }
67
+ /**
68
+ * Serves files from the dist directory
69
+ */
70
+ async function serveFile(requestPath) {
71
+ const originalFilePath = join(outDir, requestPath === '/' ? 'index.html' : requestPath);
72
+ // Use the shared pretty URL resolver
73
+ const { filePath, found } = await resolvePrettyUrl(outDir, requestPath, originalFilePath);
74
+ if (!found || !filePath) {
75
+ return {
76
+ content: requestPath.endsWith('/')
77
+ ? '404 - Directory listing not available'
78
+ : '404 - File not found',
79
+ mimeType: 'text/plain',
80
+ statusCode: 404,
81
+ };
82
+ }
83
+ try {
84
+ const mimeType = getMimeType(filePath);
85
+ const content = await readFile(filePath);
86
+ // Unlike dev server, we don't inject live reload script in preview mode
87
+ return {
88
+ content,
89
+ mimeType,
90
+ statusCode: 200,
91
+ };
92
+ }
93
+ catch {
94
+ // This should rarely happen since resolvePrettyUrl already checked the file exists
95
+ return {
96
+ content: '500 - Error reading file',
97
+ mimeType: 'text/plain',
98
+ statusCode: 500,
99
+ };
100
+ }
101
+ }
102
+ const previewServer = {
103
+ url,
104
+ async start() {
105
+ // Create HTTP server
106
+ httpServer = createServer(async (req, res) => {
107
+ const requestPath = req.url || '/';
108
+ logger.processing?.(`${req.method} ${requestPath}`);
109
+ try {
110
+ const { content, mimeType, statusCode } = await serveFile(requestPath);
111
+ res.writeHead(statusCode, {
112
+ 'Content-Type': mimeType,
113
+ 'Access-Control-Allow-Origin': '*',
114
+ 'Cache-Control': 'public, max-age=31536000', // Better caching for production preview
115
+ });
116
+ res.end(content);
117
+ }
118
+ catch (error) {
119
+ logger.error?.(`Server error: ${error instanceof Error ? error.message : String(error)}`);
120
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
121
+ res.end('500 - Internal Server Error');
122
+ }
123
+ });
124
+ // Start HTTP server
125
+ await new Promise((resolve, reject) => {
126
+ httpServer.listen(port, host, () => {
127
+ resolve();
128
+ });
129
+ httpServer.on('error', (error) => {
130
+ reject(error);
131
+ });
132
+ });
133
+ logger.success?.(`Preview server running at ${url}`);
134
+ logger.info?.(`\nServing from:`);
135
+ logger.info?.(` 📁 ${outDir}`);
136
+ // Open browser if requested
137
+ if (open) {
138
+ try {
139
+ const { default: openBrowser } = await import('open');
140
+ await openBrowser(url);
141
+ }
142
+ catch {
143
+ logger.info?.('Could not open browser automatically');
144
+ }
145
+ }
146
+ },
147
+ async stop() {
148
+ if (httpServer) {
149
+ await new Promise((resolve) => {
150
+ httpServer.close(() => {
151
+ resolve();
152
+ });
153
+ });
154
+ httpServer = null;
155
+ }
156
+ },
157
+ };
158
+ return previewServer;
159
+ }
@@ -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;AAiLzF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAS7D;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,CAkEjB"}
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;AAqLzF,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,CA0JjB"}