@stacksjs/stx 0.0.9 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +69 -21
  2. package/dist/a11y.d.ts +40 -0
  3. package/dist/analyzer.d.ts +64 -0
  4. package/dist/animation.d.ts +64 -0
  5. package/dist/assets.d.ts +19 -0
  6. package/dist/auth.d.ts +11 -0
  7. package/dist/bin/cli.js +2821 -154
  8. package/dist/caching.d.ts +18 -6
  9. package/dist/chunk-2ndtnc0t.js +9822 -0
  10. package/dist/{chunk-ywm063e4.js → chunk-e11q5a3p.js} +2 -2
  11. package/dist/chunk-vsbm352h.js +670 -0
  12. package/dist/client.d.ts +19 -35
  13. package/dist/components.d.ts +6 -0
  14. package/dist/conditionals.d.ts +17 -1
  15. package/dist/config.d.ts +6 -2
  16. package/dist/csrf.d.ts +28 -0
  17. package/dist/custom-directives.d.ts +5 -6
  18. package/dist/dev-server.d.ts +21 -0
  19. package/dist/docs.d.ts +39 -3
  20. package/dist/error-handling.d.ts +101 -0
  21. package/dist/expressions.d.ts +43 -4
  22. package/dist/formatter.d.ts +16 -0
  23. package/dist/forms.d.ts +15 -25
  24. package/dist/i18n.d.ts +17 -20
  25. package/dist/includes.d.ts +21 -18
  26. package/dist/index.d.ts +30 -24
  27. package/dist/init.d.ts +9 -0
  28. package/dist/js-ts.d.ts +10 -0
  29. package/dist/loops.d.ts +4 -3
  30. package/dist/markdown.d.ts +8 -0
  31. package/dist/method-spoofing.d.ts +13 -0
  32. package/dist/middleware.d.ts +9 -1
  33. package/dist/performance-utils.d.ts +58 -0
  34. package/dist/plugin.d.ts +2 -0
  35. package/dist/process.d.ts +9 -7
  36. package/dist/release.d.ts +1 -0
  37. package/dist/routes.d.ts +36 -0
  38. package/dist/safe-evaluator.d.ts +16 -0
  39. package/dist/seo.d.ts +33 -0
  40. package/dist/serve.d.ts +35 -0
  41. package/dist/src/index.js +2893 -135
  42. package/dist/streaming.d.ts +30 -14
  43. package/dist/types.d.ts +214 -48
  44. package/dist/utils.d.ts +21 -3
  45. package/dist/view-composers.d.ts +26 -0
  46. package/dist/web-components.d.ts +7 -14
  47. package/package.json +18 -9
  48. package/dist/chunk-04bqmpzb.js +0 -7069
  49. package/dist/chunk-8ehp5m3y.js +0 -4279
  50. package/dist/chunk-9ynf73q9.js +0 -2502
package/dist/src/index.js CHANGED
@@ -1,37 +1,52 @@
1
1
  // @bun
2
2
  import {
3
+ DEFAULT_TRANSITION_OPTIONS,
4
+ ErrorLogger,
5
+ StxError,
6
+ StxFileError,
7
+ StxRuntimeError,
8
+ StxSecurityError,
9
+ StxSyntaxError,
10
+ TransitionDirection,
11
+ TransitionEase,
12
+ TransitionType,
13
+ a11yDirective,
14
+ animationGroupDirective,
15
+ applyFilters,
3
16
  buildWebComponents,
17
+ checkA11y,
18
+ clearOnceStore,
19
+ componentDirective,
4
20
  config,
5
- defaultConfig,
6
- docsCommand,
7
- extractComponentDescription,
8
- extractComponentProps,
9
- findComponentFiles,
10
- formatDocsAsHtml,
11
- formatDocsAsJson,
12
- formatDocsAsMarkdown,
13
- generateComponentDoc,
14
- generateComponentsDocs,
15
- generateDirectivesDocs,
16
- generateDocs,
17
- generateTemplatesDocs,
18
- webComponentDirectiveHandler
19
- } from "../chunk-9ynf73q9.js";
20
- import {
21
- applyFilters,
22
21
  createDetailedErrorMessage,
22
+ createEnhancedError,
23
23
  createTranslateFilter,
24
+ defaultConfig,
24
25
  defaultFilters,
26
+ defaultI18nConfig,
27
+ defineStxConfig,
28
+ devHelpers,
29
+ errorLogger,
30
+ errorRecovery,
25
31
  escapeHtml,
26
32
  evaluateAuthExpression,
27
33
  evaluateExpression,
28
34
  extractVariables,
29
35
  fileExists,
36
+ getScreenReaderOnlyStyle,
30
37
  getSourceLineInfo,
31
38
  getTranslation,
39
+ injectSeoTags,
32
40
  loadTranslation,
41
+ markdownCache,
33
42
  markdownDirectiveHandler,
43
+ metaDirective,
44
+ motionDirective,
45
+ onceStore,
34
46
  partialsCache,
47
+ performanceMonitor,
48
+ processA11yDirectives,
49
+ processAnimationDirectives,
35
50
  processBasicFormDirectives,
36
51
  processCustomDirectives,
37
52
  processDirectives,
@@ -44,31 +59,407 @@ import {
44
59
  processJsonDirective,
45
60
  processLoops,
46
61
  processMarkdownDirectives,
62
+ processMarkdownFileDirectives,
63
+ processMetaDirectives,
47
64
  processMiddleware,
48
65
  processOnceDirective,
66
+ processSeoDirective,
49
67
  processStackPushDirectives,
50
68
  processStackReplacements,
69
+ processStructuredData,
51
70
  processTranslateDirective,
71
+ readMarkdownFile,
72
+ registerA11yDirectives,
73
+ registerAnimationDirectives,
74
+ registerComponentDirectives,
75
+ registerSeoDirectives,
52
76
  renderComponent,
53
77
  resolveTemplatePath,
54
78
  runPostProcessingMiddleware,
55
79
  runPreProcessingMiddleware,
80
+ safeExecute,
81
+ safeExecuteAsync,
82
+ scanA11yIssues,
83
+ screenReaderDirective,
84
+ scrollAnimateDirective,
56
85
  setGlobalContext,
57
- unescapeHtml
58
- } from "../chunk-8ehp5m3y.js";
59
- import"../chunk-ywm063e4.js";
60
-
61
- // ../bun-plugin/src/index.ts
86
+ structuredDataDirective,
87
+ transitionDirective,
88
+ unescapeHtml,
89
+ validators,
90
+ webComponentDirectiveHandler
91
+ } from "../chunk-2ndtnc0t.js";
92
+ import"../chunk-e11q5a3p.js";
93
+ // src/analyzer.ts
94
+ import process from "process";
95
+ async function analyzeTemplate(filePath) {
96
+ try {
97
+ const content = await Bun.file(filePath).text();
98
+ const metrics = calculateMetrics(content);
99
+ const issues = findIssues(content, filePath);
100
+ const suggestions = generateSuggestions(content, metrics);
101
+ const performance = analyzePerformance(content, metrics);
102
+ return {
103
+ file: filePath,
104
+ metrics,
105
+ issues,
106
+ suggestions,
107
+ performance
108
+ };
109
+ } catch (error) {
110
+ throw new Error(`Failed to analyze template ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+ }
113
+ function calculateMetrics(content) {
114
+ const lines = content.split(`
115
+ `);
116
+ const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
117
+ const scriptContent = scriptMatch ? scriptMatch[1] : "";
118
+ const scriptLines = scriptContent.split(`
119
+ `).length;
120
+ const directives = {
121
+ conditionals: (content.match(/@(if|unless|elseif|else|endif|endunless)\b/g) || []).length,
122
+ loops: (content.match(/@(foreach|for|endforeach|endfor|while|endwhile)\b/g) || []).length,
123
+ includes: (content.match(/@(include|component|extends|section|yield)\b/g) || []).length,
124
+ custom: 0,
125
+ total: 0
126
+ };
127
+ directives.total = directives.conditionals + directives.loops + directives.includes + directives.custom;
128
+ const regularExpressions = (content.match(/\{\{[\s\S]*?\}\}/g) || []).length;
129
+ const rawExpressions = (content.match(/\{!![\s\S]*?!!\}/g) || []).length;
130
+ const directiveExpressions = (content.match(/@(?:if|unless|elseif|foreach|for|while)\s*\([^)]+\)/g) || []).length;
131
+ const expressions = regularExpressions + rawExpressions + directiveExpressions;
132
+ const components = (content.match(/<[A-Z][^>]*>/g) || []).length;
133
+ const layouts = (content.match(/@extends\(/g) || []).length;
134
+ let maxNestingDepth = 0;
135
+ let currentDepth = 0;
136
+ for (const line of lines) {
137
+ if (/@(?:if|unless|foreach|for|while|section)\b/.test(line) && !/@end/.test(line) && !/@else/.test(line)) {
138
+ currentDepth++;
139
+ maxNestingDepth = Math.max(maxNestingDepth, currentDepth);
140
+ } else if (/@end(?:if|unless|foreach|for|while|section)\b/.test(line)) {
141
+ currentDepth--;
142
+ }
143
+ }
144
+ const nestingBonus = maxNestingDepth >= 4 ? 3 : 0;
145
+ const complexity = Math.min(10, Math.ceil(directives.total * 0.5 + expressions * 0.1 + components * 0.3 + (scriptLines > 10 ? scriptLines * 0.05 : 0) + nestingBonus));
146
+ return {
147
+ lines: lines.length,
148
+ characters: content.length,
149
+ directives,
150
+ expressions,
151
+ components,
152
+ layouts,
153
+ scriptLines,
154
+ complexity
155
+ };
156
+ }
157
+ function findIssues(content, _filePath) {
158
+ const issues = [];
159
+ const unmatchedIfs = findUnmatchedDirectives(content, "if", "endif");
160
+ if (unmatchedIfs > 0) {
161
+ issues.push({
162
+ type: "error",
163
+ category: "syntax",
164
+ message: `Found ${unmatchedIfs} unmatched @if directive(s)`,
165
+ suggestion: "Ensure every @if has a corresponding @endif"
166
+ });
167
+ }
168
+ const unmatchedForeachs = findUnmatchedDirectives(content, "foreach", "endforeach");
169
+ if (unmatchedForeachs > 0) {
170
+ issues.push({
171
+ type: "error",
172
+ category: "syntax",
173
+ message: `Found ${unmatchedForeachs} unmatched @foreach directive(s)`,
174
+ suggestion: "Ensure every @foreach has a corresponding @endforeach"
175
+ });
176
+ }
177
+ const nestedLoops = content.match(/@foreach[\s\S]*?@foreach[\s\S]*?@endforeach[\s\S]*?@endforeach/g);
178
+ if (nestedLoops && nestedLoops.length > 0) {
179
+ issues.push({
180
+ type: "warning",
181
+ category: "performance",
182
+ message: "Nested loops detected which may impact performance",
183
+ suggestion: "Consider preprocessing data or using more efficient data structures"
184
+ });
185
+ }
186
+ const rawOutputs = (content.match(/\{!![\s\S]*?!!\}/g) || []).length;
187
+ if (rawOutputs > 0) {
188
+ issues.push({
189
+ type: "warning",
190
+ category: "security",
191
+ message: `Found ${rawOutputs} raw output expression(s) {!! !!}`,
192
+ suggestion: "Ensure raw outputs are properly sanitized to prevent XSS"
193
+ });
194
+ }
195
+ const images = content.match(/<img[^>]*>/g);
196
+ if (images) {
197
+ const imagesWithoutAlt = images.filter((img) => !img.includes("alt="));
198
+ if (imagesWithoutAlt.length > 0) {
199
+ issues.push({
200
+ type: "warning",
201
+ category: "accessibility",
202
+ message: `Found ${imagesWithoutAlt.length} image(s) without alt attributes`,
203
+ suggestion: "Add alt attributes to all images for screen reader accessibility"
204
+ });
205
+ }
206
+ }
207
+ const longLines = content.split(`
208
+ `).filter((line) => line.length > 120);
209
+ if (longLines.length > 0) {
210
+ issues.push({
211
+ type: "info",
212
+ category: "maintainability",
213
+ message: `Found ${longLines.length} line(s) longer than 120 characters`,
214
+ suggestion: "Consider breaking long lines for better readability"
215
+ });
216
+ }
217
+ const inlineStyles = (content.match(/style\s*=\s*["'][^"']*["']/g) || []).length;
218
+ if (inlineStyles > 3) {
219
+ issues.push({
220
+ type: "info",
221
+ category: "maintainability",
222
+ message: `Found ${inlineStyles} inline style attributes`,
223
+ suggestion: "Consider moving styles to CSS classes for better maintainability"
224
+ });
225
+ }
226
+ return issues;
227
+ }
228
+ function generateSuggestions(content, metrics) {
229
+ const suggestions = [];
230
+ if (metrics.complexity > 7) {
231
+ suggestions.push({
232
+ type: "refactor",
233
+ message: "Template complexity is high. Consider breaking into smaller components.",
234
+ impact: "high",
235
+ effort: "medium"
236
+ });
237
+ } else if (metrics.complexity > 5 && metrics.directives.total > 8) {
238
+ suggestions.push({
239
+ type: "optimization",
240
+ message: "Template has moderate complexity. Consider component extraction for reusability.",
241
+ impact: "medium",
242
+ effort: "low"
243
+ });
244
+ }
245
+ if (metrics.lines > 100 && metrics.components === 0) {
246
+ suggestions.push({
247
+ type: "refactor",
248
+ message: "Large template with no components. Consider extracting reusable parts into components.",
249
+ impact: "medium",
250
+ effort: "medium"
251
+ });
252
+ }
253
+ if (metrics.expressions > 20) {
254
+ suggestions.push({
255
+ type: "optimization",
256
+ message: "Many template expressions detected. Consider preprocessing some data in the script section.",
257
+ impact: "medium",
258
+ effort: "low"
259
+ });
260
+ }
261
+ const nestedLoops = content.match(/@foreach[\s\S]*?@foreach[\s\S]*?@endforeach[\s\S]*?@endforeach/g);
262
+ if (nestedLoops && nestedLoops.length > 0) {
263
+ suggestions.push({
264
+ type: "optimization",
265
+ message: "Nested loops detected. Consider preprocessing data or optimizing data structures.",
266
+ impact: "high",
267
+ effort: "medium"
268
+ });
269
+ }
270
+ if (metrics.directives.total === 0 && metrics.expressions < 5) {
271
+ suggestions.push({
272
+ type: "optimization",
273
+ message: "Template appears static. Consider enabling aggressive caching.",
274
+ impact: "high",
275
+ effort: "low"
276
+ });
277
+ }
278
+ if (metrics.scriptLines > 50) {
279
+ suggestions.push({
280
+ type: "refactor",
281
+ message: "Large script section. Consider moving complex logic to external modules.",
282
+ impact: "medium",
283
+ effort: "medium"
284
+ });
285
+ }
286
+ return suggestions;
287
+ }
288
+ function analyzePerformance(content, metrics) {
289
+ let estimatedRenderTime = 1;
290
+ estimatedRenderTime += metrics.directives.conditionals * 0.1;
291
+ estimatedRenderTime += metrics.directives.loops * 0.5;
292
+ estimatedRenderTime += metrics.directives.includes * 2;
293
+ estimatedRenderTime += metrics.expressions * 0.05;
294
+ estimatedRenderTime += metrics.components * 1;
295
+ if (metrics.scriptLines > 10) {
296
+ estimatedRenderTime += metrics.scriptLines * 0.1;
297
+ }
298
+ let cacheability;
299
+ if (metrics.expressions === 0 && metrics.directives.total === 0) {
300
+ cacheability = "high";
301
+ } else if (metrics.expressions < 5 && metrics.directives.total < 3) {
302
+ cacheability = "medium";
303
+ } else {
304
+ cacheability = "low";
305
+ }
306
+ const recommendations = [];
307
+ if (estimatedRenderTime > 10) {
308
+ recommendations.push("Consider optimizing template complexity");
309
+ }
310
+ if (cacheability === "low" && metrics.directives.total > 5) {
311
+ recommendations.push("Reduce dynamic content for better caching");
312
+ }
313
+ if (metrics.components === 0 && metrics.lines > 40) {
314
+ recommendations.push("Extract components to improve reusability and performance");
315
+ }
316
+ return {
317
+ estimatedRenderTime: Math.round(estimatedRenderTime * 100) / 100,
318
+ complexityScore: metrics.complexity,
319
+ cacheability,
320
+ recommendations
321
+ };
322
+ }
323
+ function findUnmatchedDirectives(content, startDirective, endDirective) {
324
+ const contentWithoutComments = content.replace(/<!--[\s\S]*?-->/g, "");
325
+ const starts = (contentWithoutComments.match(new RegExp(`@${startDirective}\\b`, "g")) || []).length;
326
+ const ends = (contentWithoutComments.match(new RegExp(`@${endDirective}\\b`, "g")) || []).length;
327
+ return Math.abs(starts - ends);
328
+ }
329
+ async function analyzeProject(patterns = ["**/*.stx"], cwd) {
330
+ const results = [];
331
+ const allFiles = [];
332
+ for (const pattern of patterns) {
333
+ const files = await Array.fromAsync(new Bun.Glob(pattern).scan({
334
+ cwd: cwd || process.cwd(),
335
+ onlyFiles: true,
336
+ absolute: true
337
+ }));
338
+ allFiles.push(...files.filter((f) => f.endsWith(".stx")));
339
+ }
340
+ for (const file of allFiles) {
341
+ try {
342
+ const result = await analyzeTemplate(file);
343
+ results.push(result);
344
+ } catch (error) {
345
+ console.warn(`Failed to analyze ${file}:`, error);
346
+ }
347
+ }
348
+ const summary = generateProjectSummary(results);
349
+ return { results, summary };
350
+ }
351
+ function generateProjectSummary(results) {
352
+ const totalFiles = results.length;
353
+ const totalLines = results.reduce((sum, r) => sum + r.metrics.lines, 0);
354
+ const avgComplexity = totalFiles > 0 ? results.reduce((sum, r) => sum + r.metrics.complexity, 0) / totalFiles : 0;
355
+ const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
356
+ const issuesByCategory = {};
357
+ results.forEach((r) => {
358
+ r.issues.forEach((issue) => {
359
+ issuesByCategory[issue.category] = (issuesByCategory[issue.category] || 0) + 1;
360
+ });
361
+ });
362
+ const avgRenderTime = totalFiles > 0 ? results.reduce((sum, r) => sum + r.performance.estimatedRenderTime, 0) / totalFiles : 0;
363
+ const performanceScore = Math.max(1, Math.min(10, Math.round(10 - avgRenderTime / 2 - avgComplexity / 2)));
364
+ const recommendations = [];
365
+ if (avgComplexity > 6) {
366
+ recommendations.push("Project has high average complexity. Consider refactoring complex templates.");
367
+ }
368
+ if (totalIssues > totalFiles * 2) {
369
+ recommendations.push("High number of issues detected. Run detailed analysis on individual files.");
370
+ }
371
+ if (issuesByCategory.security > 0) {
372
+ recommendations.push("Security issues found. Review raw output usage and input sanitization.");
373
+ }
374
+ return {
375
+ totalFiles,
376
+ totalLines,
377
+ avgComplexity: Math.round(avgComplexity * 100) / 100,
378
+ totalIssues,
379
+ issuesByCategory,
380
+ performanceScore,
381
+ recommendations
382
+ };
383
+ }
384
+ // src/caching.ts
385
+ import fs from "fs";
62
386
  import path from "path";
387
+ var templateCache = new Map;
388
+ async function checkCache(filePath, options) {
389
+ try {
390
+ const cachePath = path.resolve(options.cachePath);
391
+ const cacheFile = path.join(cachePath, `${hashFilePath(filePath)}.html`);
392
+ const metaFile = path.join(cachePath, `${hashFilePath(filePath)}.meta.json`);
393
+ if (!await fileExists(cacheFile) || !await fileExists(metaFile))
394
+ return null;
395
+ const metaContent = await Bun.file(metaFile).text();
396
+ const meta = JSON.parse(metaContent);
397
+ if (meta.cacheVersion !== options.cacheVersion)
398
+ return null;
399
+ const stats = await fs.promises.stat(filePath);
400
+ if (stats.mtime.getTime() > meta.mtime)
401
+ return null;
402
+ for (const dep of meta.dependencies) {
403
+ if (await fileExists(dep)) {
404
+ const depStats = await fs.promises.stat(dep);
405
+ if (depStats.mtime.getTime() > meta.mtime)
406
+ return null;
407
+ } else {
408
+ return null;
409
+ }
410
+ }
411
+ return await Bun.file(cacheFile).text();
412
+ } catch (err) {
413
+ console.warn(`Cache error for ${filePath}:`, err);
414
+ return null;
415
+ }
416
+ }
417
+ async function cacheTemplate(filePath, output, dependencies, options) {
418
+ try {
419
+ const cachePath = path.resolve(options.cachePath);
420
+ await fs.promises.mkdir(cachePath, { recursive: true });
421
+ const cacheFile = path.join(cachePath, `${hashFilePath(filePath)}.html`);
422
+ const metaFile = path.join(cachePath, `${hashFilePath(filePath)}.meta.json`);
423
+ const stats = await fs.promises.stat(filePath);
424
+ await Bun.write(cacheFile, output);
425
+ const meta = {
426
+ sourcePath: filePath,
427
+ mtime: stats.mtime.getTime(),
428
+ dependencies: Array.from(dependencies),
429
+ cacheVersion: options.cacheVersion,
430
+ generatedAt: Date.now()
431
+ };
432
+ await Bun.write(metaFile, JSON.stringify(meta, null, 2));
433
+ templateCache.set(filePath, {
434
+ output,
435
+ mtime: stats.mtime.getTime(),
436
+ dependencies
437
+ });
438
+ } catch (err) {
439
+ console.warn(`Failed to cache template ${filePath}:`, err);
440
+ }
441
+ }
442
+ function hashFilePath(filePath) {
443
+ const hash = new Bun.CryptoHasher("sha1").update(filePath).digest("hex");
444
+ return hash.substring(0, 16);
445
+ }
446
+ // src/dev-server.ts
447
+ var {serve } = globalThis.Bun;
448
+ import fs2 from "fs";
449
+ import path3 from "path";
450
+ import process2 from "process";
451
+
452
+ // src/plugin.ts
453
+ import path2 from "path";
63
454
  var plugin = {
64
455
  name: "bun-plugin-stx",
65
456
  async setup(build) {
66
457
  const options = {
67
- ...defaultConfig,
458
+ ...config,
68
459
  ...build.config?.stx
69
460
  };
70
461
  const allDependencies = new Set;
71
- const webComponentsPath = options.webComponents?.enabled ? `./${path.relative(path.dirname(build.config?.outdir || "dist"), options.webComponents.outputDir || "dist/web-components")}` : "/web-components";
462
+ const webComponentsPath = options.webComponents?.enabled ? `./${path2.relative(path2.dirname(build.config?.outdir || "dist"), options.webComponents.outputDir || "dist/web-components")}` : "/web-components";
72
463
  const builtComponents = [];
73
464
  if (options.webComponents?.enabled) {
74
465
  try {
@@ -77,132 +468,2440 @@ var plugin = {
77
468
  if (options.debug && components.length > 0) {
78
469
  console.log(`Successfully built ${components.length} web components`);
79
470
  }
80
- } catch (error) {
81
- console.error("Failed to build web components:", error);
471
+ } catch (error) {
472
+ console.error("Failed to build web components:", error);
473
+ }
474
+ }
475
+ build.onLoad({ filter: /\.stx$/ }, async ({ path: filePath }) => {
476
+ try {
477
+ const dependencies = new Set;
478
+ if (options.cache && options.cachePath) {
479
+ const cachedOutput = await checkCache(filePath, options);
480
+ if (cachedOutput) {
481
+ if (options.debug) {
482
+ console.log(`Using cached version of ${filePath}`);
483
+ }
484
+ return {
485
+ contents: cachedOutput,
486
+ loader: "html"
487
+ };
488
+ }
489
+ }
490
+ const content = await safeExecuteAsync(() => Bun.file(filePath).text(), "", (error) => {
491
+ throw new StxFileError(`Failed to read stx file: ${filePath}`, filePath, undefined, undefined, `File read error: ${error.message}`);
492
+ });
493
+ if (!content) {
494
+ throw new StxFileError(`stx file is empty: ${filePath}`, filePath);
495
+ }
496
+ const { scriptContent, templateContent } = performanceMonitor.time("script-extraction", () => {
497
+ const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
498
+ const scriptContent2 = scriptMatch ? scriptMatch[1] : "";
499
+ const templateContent2 = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, "");
500
+ return { scriptContent: scriptContent2, templateContent: templateContent2 };
501
+ });
502
+ const context = {
503
+ __filename: filePath,
504
+ __dirname: path2.dirname(filePath),
505
+ __stx: {
506
+ webComponentsPath,
507
+ builtComponents
508
+ }
509
+ };
510
+ await safeExecuteAsync(() => extractVariables(scriptContent, context, filePath), undefined, (error) => {
511
+ const scriptError = new StxRuntimeError(`Script execution failed in ${filePath}: ${error.message}`, filePath, undefined, undefined, scriptContent.substring(0, 100));
512
+ errorLogger.log(scriptError, { filePath, scriptContent });
513
+ if (options.debug) {
514
+ throw scriptError;
515
+ }
516
+ });
517
+ const output = await performanceMonitor.timeAsync("directive-processing", async () => {
518
+ return await processDirectives(templateContent, context, filePath, options, dependencies);
519
+ });
520
+ dependencies.forEach((dep) => allDependencies.add(dep));
521
+ if (options.cache && options.cachePath) {
522
+ await cacheTemplate(filePath, output, dependencies, options);
523
+ if (options.debug) {
524
+ console.log(`Cached template ${filePath} with ${dependencies.size} dependencies`);
525
+ }
526
+ }
527
+ return {
528
+ contents: output,
529
+ loader: "html"
530
+ };
531
+ } catch (error) {
532
+ const enhancedError = error instanceof Error ? error : new StxRuntimeError(`Plugin processing failed: ${String(error)}`, filePath);
533
+ errorLogger.log(enhancedError, { filePath, buildConfig: build.config });
534
+ devHelpers.logDetailedError(enhancedError, { filePath, plugin: "bun-plugin-stx" });
535
+ if (options.debug) {
536
+ console.error("stx Plugin Error:", enhancedError);
537
+ }
538
+ const stxError = enhancedError;
539
+ const errorLine = stxError.line || null;
540
+ const errorContext = stxError.context || null;
541
+ const errorHtml = `<!DOCTYPE html>
542
+ <html lang="en">
543
+ <head>
544
+ <meta charset="UTF-8">
545
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
546
+ <title>stx Rendering Error</title>
547
+ <style>
548
+ body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; }
549
+ .error-header { color: #dc3545; border-left: 4px solid #dc3545; padding-left: 16px; margin-bottom: 24px; }
550
+ .error-details { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 16px; margin: 16px 0; }
551
+ .error-code { background: #2d3748; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; font-family: 'Monaco', 'Menlo', monospace; font-size: 14px; }
552
+ .help-section { background: #e3f2fd; border-left: 4px solid #2196f3; padding: 16px; margin-top: 24px; }
553
+ .file-path { font-family: monospace; background: #f1f3f4; padding: 2px 6px; border-radius: 4px; }
554
+ </style>
555
+ </head>
556
+ <body>
557
+ <div class="error-header">
558
+ <h1>stx Template Error</h1>
559
+ <p>An error occurred while processing your stx template.</p>
560
+ </div>
561
+
562
+ <div class="error-details">
563
+ <h3>Error Details</h3>
564
+ <p><strong>File:</strong> <span class="file-path">${filePath}</span></p>
565
+ <p><strong>Error:</strong> ${enhancedError.message}</p>
566
+ ${errorLine ? `<p><strong>Line:</strong> ${errorLine}</p>` : ""}
567
+ ${errorContext ? `<p><strong>Context:</strong> ${errorContext}</p>` : ""}
568
+ </div>
569
+
570
+ ${enhancedError.stack ? `
571
+ <div class="error-details">
572
+ <h3>Stack Trace</h3>
573
+ <pre class="error-code">${enhancedError.stack}</pre>
574
+ </div>` : ""}
575
+
576
+ <div class="help-section">
577
+ <h3>\uD83D\uDCA1 Troubleshooting Tips</h3>
578
+ <ul>
579
+ <li>Check the syntax of your stx directives (e.g., @if, @foreach)</li>
580
+ <li>Verify that all variables used in the template are properly defined</li>
581
+ <li>Ensure script tags have valid JavaScript/TypeScript syntax</li>
582
+ <li>Run <code>stx debug ${path2.basename(filePath)}</code> for detailed analysis</li>
583
+ <li>Enable debug mode in your stx config for more detailed error messages</li>
584
+ </ul>
585
+ </div>
586
+ </body>
587
+ </html>`;
588
+ return {
589
+ contents: errorHtml,
590
+ loader: "html"
591
+ };
592
+ }
593
+ });
594
+ }
595
+ };
596
+
597
+ // src/dev-server.ts
598
+ async function findAvailablePort(startPort, maxAttempts = 10) {
599
+ for (let i = 0;i < maxAttempts; i++) {
600
+ const port = startPort + i;
601
+ try {
602
+ const testServer = serve({
603
+ port,
604
+ fetch: () => new Response("test")
605
+ });
606
+ testServer.stop();
607
+ return port;
608
+ } catch (error) {
609
+ if (error.code === "EADDRINUSE") {
610
+ continue;
611
+ }
612
+ throw error;
613
+ }
614
+ }
615
+ throw new Error(`Could not find an available port between ${startPort} and ${startPort + maxAttempts - 1}`);
616
+ }
617
+ var colors = {
618
+ reset: "\x1B[0m",
619
+ bright: "\x1B[1m",
620
+ dim: "\x1B[2m",
621
+ underscore: "\x1B[4m",
622
+ blink: "\x1B[5m",
623
+ reverse: "\x1B[7m",
624
+ hidden: "\x1B[8m",
625
+ black: "\x1B[30m",
626
+ red: "\x1B[31m",
627
+ green: "\x1B[32m",
628
+ yellow: "\x1B[33m",
629
+ blue: "\x1B[34m",
630
+ magenta: "\x1B[35m",
631
+ cyan: "\x1B[36m",
632
+ white: "\x1B[37m",
633
+ gray: "\x1B[90m",
634
+ bgBlack: "\x1B[40m",
635
+ bgRed: "\x1B[41m",
636
+ bgGreen: "\x1B[42m",
637
+ bgYellow: "\x1B[43m",
638
+ bgBlue: "\x1B[44m",
639
+ bgMagenta: "\x1B[45m",
640
+ bgCyan: "\x1B[46m",
641
+ bgWhite: "\x1B[47m",
642
+ bgGray: "\x1B[100m"
643
+ };
644
+ function setupKeyboardShortcuts(serverUrl, stopServer) {
645
+ if (process2.stdin.isTTY) {
646
+ process2.stdin.setRawMode(true);
647
+ process2.stdin.setEncoding("utf8");
648
+ process2.stdin.resume();
649
+ console.log(`
650
+ Keyboard shortcuts:`);
651
+ console.log(` ${colors.cyan}o${colors.reset} + Enter - Open in browser`);
652
+ console.log(` ${colors.cyan}c${colors.reset} + Enter - Clear console`);
653
+ console.log(` ${colors.cyan}q${colors.reset} + Enter (or ${colors.cyan}Ctrl+C${colors.reset}) - Quit server`);
654
+ let buffer = "";
655
+ process2.stdin.on("data", (key) => {
656
+ if (key === "\x03") {
657
+ stopServer();
658
+ process2.exit(0);
659
+ }
660
+ buffer += key;
661
+ if (buffer.endsWith("\r") || buffer.endsWith(`
662
+ `)) {
663
+ const cmd = buffer.trim().toLowerCase();
664
+ buffer = "";
665
+ if (cmd === "o") {
666
+ console.log(`${colors.dim}Opening ${colors.cyan}${serverUrl}${colors.dim} in your browser...${colors.reset}`);
667
+ Bun.spawn(["open", serverUrl], { stderr: "inherit" });
668
+ } else if (cmd === "c") {
669
+ console.clear();
670
+ console.log(`${colors.green}Server running at ${colors.cyan}${serverUrl}${colors.reset}`);
671
+ console.log(`Press ${colors.cyan}Ctrl+C${colors.reset} to stop the server`);
672
+ console.log(`
673
+ Keyboard shortcuts:`);
674
+ console.log(` ${colors.cyan}o${colors.reset} + Enter - Open in browser`);
675
+ console.log(` ${colors.cyan}c${colors.reset} + Enter - Clear console`);
676
+ console.log(` ${colors.cyan}q${colors.reset} + Enter (or ${colors.cyan}Ctrl+C${colors.reset}) - Quit server`);
677
+ } else if (cmd === "q") {
678
+ console.log(`${colors.yellow}Stopping server...${colors.reset}`);
679
+ stopServer();
680
+ process2.exit(0);
681
+ } else if (cmd === "h") {
682
+ console.log(`
683
+ Keyboard Shortcuts:`);
684
+ console.log(` ${colors.cyan}o${colors.reset} + Enter - Open in browser`);
685
+ console.log(` ${colors.cyan}c${colors.reset} + Enter - Clear console`);
686
+ console.log(` ${colors.cyan}q${colors.reset} + Enter (or ${colors.cyan}Ctrl+C${colors.reset}) - Quit server`);
687
+ console.log(` ${colors.cyan}h${colors.reset} + Enter - Show this help`);
688
+ }
689
+ }
690
+ });
691
+ }
692
+ }
693
+ async function serveMarkdownFile(filePath, options = {}) {
694
+ const port = options.port || 3000;
695
+ const watch = options.watch !== false;
696
+ const absolutePath = path3.resolve(filePath);
697
+ if (!fs2.existsSync(absolutePath)) {
698
+ console.error(`${colors.red}Error: File not found: ${colors.bright}${absolutePath}${colors.reset}`);
699
+ return false;
700
+ }
701
+ if (!absolutePath.endsWith(".md")) {
702
+ console.error(`${colors.red}Error: File must have .md extension: ${colors.bright}${absolutePath}${colors.reset}`);
703
+ return false;
704
+ }
705
+ console.log(`${colors.blue}Processing${colors.reset} ${colors.bright}${filePath}${colors.reset}...`);
706
+ let htmlContent = null;
707
+ const processFile = async () => {
708
+ try {
709
+ const { content, data } = await readMarkdownFile(absolutePath, {
710
+ markdown: {
711
+ syntaxHighlighting: {
712
+ serverSide: true,
713
+ enabled: true,
714
+ defaultTheme: config.markdown?.syntaxHighlighting?.defaultTheme || "github-dark",
715
+ highlightUnknownLanguages: true
716
+ }
717
+ }
718
+ });
719
+ const markdownConfig = options.markdown?.syntaxHighlighting || config.markdown?.syntaxHighlighting;
720
+ const defaultTheme = markdownConfig?.defaultTheme || "github-dark";
721
+ const baseThemes = ["github-dark"];
722
+ const configThemes = markdownConfig?.additionalThemes || [];
723
+ const availableThemes = [...new Set([...baseThemes, ...configThemes])];
724
+ const themeOptions = availableThemes.map((theme) => `<option value="${theme}"${theme === defaultTheme ? " selected" : ""}>${theme}</option>`).join(`
725
+ `);
726
+ htmlContent = `
727
+ <!DOCTYPE html>
728
+ <html lang="en">
729
+ <head>
730
+ <meta charset="UTF-8">
731
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
732
+ <title>${data.title || path3.basename(absolutePath)}</title>
733
+ <!-- Syntax highlighting styles -->
734
+ <style id="syntax-theme">
735
+ :root {
736
+ --shiki-color-text: #24292e;
737
+ --shiki-color-background: #ffffff;
738
+ --shiki-token-constant: #005cc5;
739
+ --shiki-token-string: #032f62;
740
+ --shiki-token-comment: #6a737d;
741
+ --shiki-token-keyword: #d73a49;
742
+ --shiki-token-parameter: #24292e;
743
+ --shiki-token-function: #6f42c1;
744
+ --shiki-token-string-expression: #032f62;
745
+ --shiki-token-punctuation: #24292e;
746
+ --shiki-token-link: #032f62;
747
+ }
748
+ pre {
749
+ background-color: var(--shiki-color-background);
750
+ padding: 1rem;
751
+ border-radius: 4px;
752
+ }
753
+ code {
754
+ color: var(--shiki-color-text);
755
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
756
+ }
757
+ .dark-mode {
758
+ --shiki-color-text: #e1e4e8;
759
+ --shiki-color-background: #24292e;
760
+ --shiki-token-constant: #79b8ff;
761
+ --shiki-token-string: #9ecbff;
762
+ --shiki-token-comment: #6a737d;
763
+ --shiki-token-keyword: #f97583;
764
+ --shiki-token-parameter: #e1e4e8;
765
+ --shiki-token-function: #b392f0;
766
+ --shiki-token-string-expression: #9ecbff;
767
+ --shiki-token-punctuation: #e1e4e8;
768
+ --shiki-token-link: #9ecbff;
769
+ }
770
+ </style>
771
+ <style>
772
+ body {
773
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
774
+ line-height: 1.6;
775
+ color: #333;
776
+ max-width: 800px;
777
+ margin: 0 auto;
778
+ padding: 2rem;
779
+ }
780
+ pre, code {
781
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
782
+ }
783
+ pre {
784
+ border-radius: 4px;
785
+ padding: 0;
786
+ margin: 1.5rem 0;
787
+ overflow-x: auto;
788
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
789
+ }
790
+ pre code {
791
+ display: block;
792
+ padding: 1rem;
793
+ overflow-x: auto;
794
+ }
795
+ /* Apply background color to code blocks based on theme */
796
+ pre.syntax-highlighter {
797
+ background-color: var(--shiki-color-background) !important;
798
+ }
799
+ .dark-mode pre.syntax-highlighter {
800
+ background-color: var(--shiki-color-background) !important;
801
+ }
802
+ code {
803
+ padding: 0.2rem 0.4rem;
804
+ border-radius: 3px;
805
+ }
806
+ h1, h2, h3, h4 {
807
+ margin-top: 2rem;
808
+ margin-bottom: 1rem;
809
+ }
810
+ h1 { color: #111; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
811
+ h2 { color: #333; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.3rem; }
812
+ h3 { color: #444; }
813
+ img {
814
+ max-width: 100%;
815
+ border-radius: 4px;
816
+ margin: 1rem 0;
817
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
818
+ }
819
+ blockquote {
820
+ border-left: 4px solid #ddd;
821
+ padding-left: 1rem;
822
+ margin-left: 0;
823
+ color: #555;
824
+ background: #f9f9f9;
825
+ padding: 0.5rem 1rem;
826
+ margin: 1.5rem 0;
827
+ }
828
+ table {
829
+ border-collapse: collapse;
830
+ width: 100%;
831
+ margin: 1.5rem 0;
832
+ }
833
+ th, td {
834
+ border: 1px solid #ddd;
835
+ padding: 0.5rem;
836
+ }
837
+ th {
838
+ background: #f0f0f0;
839
+ text-align: left;
840
+ }
841
+ tr:nth-child(even) {
842
+ background-color: #f8f8f8;
843
+ }
844
+ a {
845
+ color: #0066cc;
846
+ text-decoration: none;
847
+ }
848
+ a:hover {
849
+ text-decoration: underline;
850
+ }
851
+ hr {
852
+ border: 0;
853
+ border-top: 1px solid #eee;
854
+ margin: 2rem 0;
855
+ }
856
+ .frontmatter {
857
+ background: #f8f8f8;
858
+ border-radius: 4px;
859
+ padding: 1.5rem;
860
+ margin-bottom: 2rem;
861
+ font-size: 0.9rem;
862
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
863
+ border-left: 4px solid #ddd;
864
+ }
865
+ .frontmatter-item {
866
+ margin-bottom: 0.5rem;
867
+ display: flex;
868
+ }
869
+ .frontmatter-label {
870
+ font-weight: bold;
871
+ min-width: 100px;
872
+ color: #555;
873
+ }
874
+ /* Theme selector for code blocks */
875
+ .theme-selector {
876
+ margin: 1rem 0;
877
+ padding: 0.5rem;
878
+ background: #f8f8f8;
879
+ border-radius: 4px;
880
+ text-align: right;
881
+ }
882
+ select {
883
+ padding: 0.25rem 0.5rem;
884
+ border-radius: 3px;
885
+ border: 1px solid #ddd;
886
+ }
887
+
888
+ /* Dark mode body styles */
889
+ body.dark-mode {
890
+ background-color: #121212;
891
+ color: #e1e4e8;
892
+ }
893
+
894
+ body.dark-mode h1,
895
+ body.dark-mode h2,
896
+ body.dark-mode h3 {
897
+ color: #e1e4e8;
898
+ border-color: #2f363d;
899
+ }
900
+
901
+ body.dark-mode .theme-selector {
902
+ background: #2f363d;
903
+ color: #e1e4e8;
904
+ }
905
+
906
+ body.dark-mode select {
907
+ background: #24292e;
908
+ color: #e1e4e8;
909
+ border-color: #444;
910
+ }
911
+
912
+ body.dark-mode blockquote {
913
+ background: #24292e;
914
+ color: #e1e4e8;
915
+ }
916
+
917
+ body.dark-mode .frontmatter {
918
+ background: #24292e;
919
+ }
920
+
921
+ body.dark-mode a {
922
+ color: #58a6ff;
923
+ }
924
+ </style>
925
+ </head>
926
+ <body>
927
+ ${Object.keys(data).length > 0 ? `
928
+ <div class="frontmatter">
929
+ <h3>Frontmatter</h3>
930
+ ${Object.entries(data).map(([key, value]) => `
931
+ <div class="frontmatter-item">
932
+ <span class="frontmatter-label">${key}:</span>
933
+ <span>${Array.isArray(value) ? value.join(", ") : value}</span>
934
+ </div>`).join("")}
935
+ </div>
936
+ ` : ""}
937
+
938
+ <div class="theme-selector">
939
+ Theme:
940
+ <select id="themeSelector" onchange="changeTheme()">
941
+ ${themeOptions}
942
+ </select>
943
+ </div>
944
+
945
+ ${content}
946
+
947
+ <script>
948
+ function changeTheme() {
949
+ const theme = document.getElementById('themeSelector').value;
950
+
951
+ // Toggle dark mode class based on theme
952
+ if (theme.includes('dark') || theme.includes('night') || theme.includes('monokai') ||
953
+ theme.includes('dracula') || theme.includes('nord') || theme.includes('material')) {
954
+ document.body.classList.add('dark-mode');
955
+ } else {
956
+ document.body.classList.remove('dark-mode');
957
+ }
958
+ }
959
+
960
+ // Initialize theme on page load
961
+ document.addEventListener('DOMContentLoaded', function() {
962
+ changeTheme();
963
+ });
964
+ </script>
965
+ </body>
966
+ </html>
967
+ `;
968
+ return true;
969
+ } catch (error) {
970
+ console.error(`${colors.red}Error processing Markdown file:${colors.reset}`, error);
971
+ return false;
972
+ }
973
+ };
974
+ const processSuccess = await processFile();
975
+ if (!processSuccess) {
976
+ return false;
977
+ }
978
+ let actualPort = port;
979
+ try {
980
+ actualPort = await findAvailablePort(port);
981
+ if (actualPort !== port) {
982
+ console.log(`${colors.yellow}Port ${port} is busy, using port ${actualPort} instead${colors.reset}`);
983
+ }
984
+ } catch {
985
+ console.error(`${colors.red}Could not find an available port${colors.reset}`);
986
+ return false;
987
+ }
988
+ console.log(`${colors.blue}Starting server on ${colors.cyan}http://localhost:${actualPort}/${colors.reset}...`);
989
+ const server = serve({
990
+ port: actualPort,
991
+ fetch(request) {
992
+ const url = new URL(request.url);
993
+ if (url.pathname === "/") {
994
+ return new Response(htmlContent, {
995
+ headers: {
996
+ "Content-Type": "text/html"
997
+ }
998
+ });
999
+ }
1000
+ return new Response("Not Found", { status: 404 });
1001
+ },
1002
+ error(error) {
1003
+ return new Response(`<pre>${error}
1004
+ ${error.stack}</pre>`, {
1005
+ headers: {
1006
+ "Content-Type": "text/html"
1007
+ }
1008
+ });
1009
+ }
1010
+ });
1011
+ console.clear();
1012
+ console.log(`
1013
+ ${colors.blue}stx${colors.reset} ${colors.green}${process2.env.stx_VERSION || "v0.0.10"}${colors.reset} ${colors.dim}ready in ${Math.random() * 10 + 5 | 0}.${Math.random() * 90 + 10 | 0} ms${colors.reset}`);
1014
+ console.log(`
1015
+ ${colors.bright}\u2192 ${colors.cyan}http://localhost:${port}/${colors.reset}`);
1016
+ console.log(`
1017
+ ${colors.yellow}Routes:${colors.reset}`);
1018
+ const relativeFilePath = path3.relative(process2.cwd(), absolutePath);
1019
+ console.log(` ${colors.green}\u2514\u2500 /${colors.reset} \u2192 ${colors.bright}${relativeFilePath}${colors.reset}`);
1020
+ console.log(`
1021
+ Press ${colors.cyan}h${colors.reset} + ${colors.cyan}Enter${colors.reset} to show shortcuts`);
1022
+ setupKeyboardShortcuts(server.url.toString(), () => {
1023
+ if (watch) {
1024
+ const watcher = fs2.watch(path3.dirname(absolutePath), { recursive: true });
1025
+ watcher.close();
1026
+ }
1027
+ server.stop();
1028
+ });
1029
+ if (watch) {
1030
+ const dirToWatch = path3.dirname(absolutePath);
1031
+ console.log(`${colors.blue}Watching ${colors.bright}${dirToWatch}${colors.reset} for changes...`);
1032
+ const watcher = fs2.watch(dirToWatch, { recursive: true }, async (eventType, filename) => {
1033
+ if (filename && filename.endsWith(".md")) {
1034
+ console.log(`${colors.yellow}File ${colors.bright}${filename}${colors.yellow} changed, reprocessing...${colors.reset}`);
1035
+ await processFile();
1036
+ }
1037
+ });
1038
+ process2.on("SIGINT", () => {
1039
+ watcher.close();
1040
+ server.stop();
1041
+ process2.exit(0);
1042
+ });
1043
+ }
1044
+ return true;
1045
+ }
1046
+ async function serveStxFile(filePath, options = {}) {
1047
+ const port = options.port || 3000;
1048
+ const watch = options.watch !== false;
1049
+ const absolutePath = path3.resolve(filePath);
1050
+ if (!fs2.existsSync(absolutePath)) {
1051
+ console.error(`${colors.red}Error: File not found: ${colors.bright}${absolutePath}${colors.reset}`);
1052
+ return false;
1053
+ }
1054
+ if (absolutePath.endsWith(".md")) {
1055
+ return serveMarkdownFile(absolutePath, options);
1056
+ } else if (!absolutePath.endsWith(".stx")) {
1057
+ console.error(`${colors.red}Error: Unsupported file type: ${colors.bright}${absolutePath}${colors.reset}. Only .stx and .md files are supported.`);
1058
+ return false;
1059
+ }
1060
+ const outputDir = path3.join(process2.cwd(), ".stx-output");
1061
+ fs2.mkdirSync(outputDir, { recursive: true });
1062
+ console.log(`${colors.blue}Building ${colors.bright}${filePath}${colors.reset}...`);
1063
+ let htmlContent = null;
1064
+ const buildFile = async () => {
1065
+ try {
1066
+ const result = await Bun.build({
1067
+ entrypoints: [absolutePath],
1068
+ outdir: outputDir,
1069
+ plugins: [plugin],
1070
+ define: {
1071
+ "process.env.NODE_ENV": '"development"'
1072
+ },
1073
+ ...options.stxOptions
1074
+ });
1075
+ if (!result.success) {
1076
+ console.error(`${colors.red}Build failed:${colors.reset}`, result.logs);
1077
+ return false;
1078
+ }
1079
+ const htmlOutput = result.outputs.find((o) => o.path.endsWith(".html"));
1080
+ if (!htmlOutput) {
1081
+ console.error(`${colors.red}No HTML output found${colors.reset}`);
1082
+ return false;
1083
+ }
1084
+ const html = await Bun.file(htmlOutput.path).text();
1085
+ htmlContent = html;
1086
+ return true;
1087
+ } catch (error) {
1088
+ console.error(`${colors.red}Error building stx file:${colors.reset}`, error);
1089
+ return false;
1090
+ }
1091
+ };
1092
+ const buildSuccess = await buildFile();
1093
+ if (!buildSuccess) {
1094
+ return false;
1095
+ }
1096
+ let actualPort = port;
1097
+ try {
1098
+ actualPort = await findAvailablePort(port);
1099
+ if (actualPort !== port) {
1100
+ console.log(`${colors.yellow}Port ${port} is busy, using port ${actualPort} instead${colors.reset}`);
1101
+ }
1102
+ } catch {
1103
+ console.error(`${colors.red}Could not find an available port${colors.reset}`);
1104
+ return false;
1105
+ }
1106
+ console.log(`${colors.blue}Starting server on ${colors.cyan}http://localhost:${actualPort}/${colors.reset}...`);
1107
+ const server = serve({
1108
+ port: actualPort,
1109
+ fetch(request) {
1110
+ const url = new URL(request.url);
1111
+ if (url.pathname === "/") {
1112
+ return new Response(htmlContent, {
1113
+ headers: {
1114
+ "Content-Type": "text/html"
1115
+ }
1116
+ });
1117
+ }
1118
+ const requestedPath = path3.join(outputDir, url.pathname);
1119
+ if (fs2.existsSync(requestedPath) && fs2.statSync(requestedPath).isFile()) {
1120
+ const file = Bun.file(requestedPath);
1121
+ const ext = path3.extname(requestedPath).toLowerCase();
1122
+ let contentType = "text/plain";
1123
+ switch (ext) {
1124
+ case ".html":
1125
+ contentType = "text/html";
1126
+ break;
1127
+ case ".css":
1128
+ contentType = "text/css";
1129
+ break;
1130
+ case ".js":
1131
+ contentType = "text/javascript";
1132
+ break;
1133
+ case ".json":
1134
+ contentType = "application/json";
1135
+ break;
1136
+ case ".png":
1137
+ contentType = "image/png";
1138
+ break;
1139
+ case ".jpg":
1140
+ case ".jpeg":
1141
+ contentType = "image/jpeg";
1142
+ break;
1143
+ case ".gif":
1144
+ contentType = "image/gif";
1145
+ break;
1146
+ }
1147
+ return new Response(file, {
1148
+ headers: { "Content-Type": contentType }
1149
+ });
1150
+ }
1151
+ return new Response("Not Found", { status: 404 });
1152
+ },
1153
+ error(error) {
1154
+ return new Response(`<pre>${error}
1155
+ ${error.stack}</pre>`, {
1156
+ headers: {
1157
+ "Content-Type": "text/html"
1158
+ }
1159
+ });
1160
+ }
1161
+ });
1162
+ console.clear();
1163
+ console.log(`
1164
+ ${colors.blue}stx${colors.reset} ${colors.green}${process2.env.stx_VERSION || "v0.0.10"}${colors.reset} ${colors.dim}ready in ${Math.random() * 10 + 5 | 0}.${Math.random() * 90 + 10 | 0} ms${colors.reset}`);
1165
+ console.log(`
1166
+ ${colors.bright}\u2192 ${colors.cyan}http://localhost:${port}/${colors.reset}`);
1167
+ console.log(`
1168
+ ${colors.yellow}Routes:${colors.reset}`);
1169
+ const relativeFilePath = path3.relative(process2.cwd(), absolutePath);
1170
+ console.log(` ${colors.green}\u2514\u2500 /${colors.reset} \u2192 ${colors.bright}${relativeFilePath}${colors.reset}`);
1171
+ console.log(`
1172
+ Press ${colors.cyan}h${colors.reset} + ${colors.cyan}Enter${colors.reset} to show shortcuts`);
1173
+ setupKeyboardShortcuts(server.url.toString(), () => {
1174
+ if (watch) {
1175
+ const watcher = fs2.watch(path3.dirname(absolutePath), { recursive: true });
1176
+ watcher.close();
1177
+ }
1178
+ server.stop();
1179
+ });
1180
+ if (watch) {
1181
+ const dirToWatch = path3.dirname(absolutePath);
1182
+ console.log(`${colors.blue}Watching ${colors.bright}${dirToWatch}${colors.reset} for changes...`);
1183
+ const watcher = fs2.watch(dirToWatch, { recursive: true }, async (eventType, filename) => {
1184
+ if (filename && (filename.endsWith(".stx") || filename.endsWith(".js") || filename.endsWith(".ts"))) {
1185
+ console.log(`${colors.yellow}File ${colors.bright}${filename}${colors.yellow} changed, rebuilding...${colors.reset}`);
1186
+ await buildFile();
1187
+ }
1188
+ });
1189
+ process2.on("SIGINT", () => {
1190
+ watcher.close();
1191
+ server.stop();
1192
+ process2.exit(0);
1193
+ });
1194
+ }
1195
+ return true;
1196
+ }
1197
+ async function serveMultipleStxFiles(filePaths, options = {}) {
1198
+ const port = options.port || 3000;
1199
+ const watch = options.watch !== false;
1200
+ for (const filePath of filePaths) {
1201
+ const absolutePath = path3.resolve(filePath);
1202
+ if (!fs2.existsSync(absolutePath)) {
1203
+ console.error(`${colors.red}Error: File not found: ${colors.bright}${absolutePath}${colors.reset}`);
1204
+ return false;
1205
+ }
1206
+ if (!absolutePath.endsWith(".stx") && !absolutePath.endsWith(".md")) {
1207
+ console.error(`${colors.red}Error: Unsupported file type: ${colors.bright}${absolutePath}${colors.reset}. Only .stx and .md files are supported.`);
1208
+ return false;
1209
+ }
1210
+ }
1211
+ const outputDir = path3.join(process2.cwd(), ".stx-output");
1212
+ fs2.mkdirSync(outputDir, { recursive: true });
1213
+ const commonDir = findCommonDir(filePaths.map((f) => path3.dirname(path3.resolve(f))));
1214
+ console.log(`${colors.blue}Processing ${colors.bright}${filePaths.length}${colors.reset} files...`);
1215
+ const routes = {};
1216
+ const buildFiles = async () => {
1217
+ try {
1218
+ for (const filePath of filePaths) {
1219
+ const absolutePath = path3.resolve(filePath);
1220
+ const isMarkdown = absolutePath.endsWith(".md");
1221
+ if (isMarkdown) {
1222
+ try {
1223
+ const { content, data } = await readMarkdownFile(absolutePath, {
1224
+ markdown: {
1225
+ syntaxHighlighting: {
1226
+ serverSide: true,
1227
+ enabled: true,
1228
+ defaultTheme: config.markdown?.syntaxHighlighting?.defaultTheme || "github-dark",
1229
+ highlightUnknownLanguages: true
1230
+ }
1231
+ }
1232
+ });
1233
+ const markdownConfig = options.markdown?.syntaxHighlighting || config.markdown?.syntaxHighlighting;
1234
+ const defaultTheme = markdownConfig?.defaultTheme || "github-dark";
1235
+ const baseThemes = ["github-dark"];
1236
+ const configThemes = markdownConfig?.additionalThemes || [];
1237
+ const availableThemes = [...new Set([...baseThemes, ...configThemes])];
1238
+ const themeOptions = availableThemes.map((theme) => `<option value="${theme}"${theme === defaultTheme ? " selected" : ""}>${theme}</option>`).join(`
1239
+ `);
1240
+ const htmlContent = `
1241
+ <!DOCTYPE html>
1242
+ <html lang="en">
1243
+ <head>
1244
+ <meta charset="UTF-8">
1245
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1246
+ <title>${data.title || path3.basename(absolutePath)}</title>
1247
+ <!-- Syntax highlighting styles -->
1248
+ <style id="syntax-theme">
1249
+ :root {
1250
+ --shiki-color-text: #24292e;
1251
+ --shiki-color-background: #ffffff;
1252
+ --shiki-token-constant: #005cc5;
1253
+ --shiki-token-string: #032f62;
1254
+ --shiki-token-comment: #6a737d;
1255
+ --shiki-token-keyword: #d73a49;
1256
+ --shiki-token-parameter: #24292e;
1257
+ --shiki-token-function: #6f42c1;
1258
+ --shiki-token-string-expression: #032f62;
1259
+ --shiki-token-punctuation: #24292e;
1260
+ --shiki-token-link: #032f62;
1261
+ }
1262
+ pre {
1263
+ background-color: var(--shiki-color-background);
1264
+ padding: 1rem;
1265
+ border-radius: 4px;
1266
+ }
1267
+ code {
1268
+ color: var(--shiki-color-text);
1269
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1270
+ }
1271
+ .dark-mode {
1272
+ --shiki-color-text: #e1e4e8;
1273
+ --shiki-color-background: #24292e;
1274
+ --shiki-token-constant: #79b8ff;
1275
+ --shiki-token-string: #9ecbff;
1276
+ --shiki-token-comment: #6a737d;
1277
+ --shiki-token-keyword: #f97583;
1278
+ --shiki-token-parameter: #e1e4e8;
1279
+ --shiki-token-function: #b392f0;
1280
+ --shiki-token-string-expression: #9ecbff;
1281
+ --shiki-token-punctuation: #e1e4e8;
1282
+ --shiki-token-link: #9ecbff;
1283
+ }
1284
+ </style>
1285
+ <style>
1286
+ body {
1287
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
1288
+ line-height: 1.6;
1289
+ color: #333;
1290
+ max-width: 800px;
1291
+ margin: 0 auto;
1292
+ padding: 2rem;
1293
+ }
1294
+ pre, code {
1295
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1296
+ }
1297
+ pre {
1298
+ border-radius: 4px;
1299
+ padding: 0;
1300
+ margin: 1.5rem 0;
1301
+ overflow-x: auto;
1302
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
1303
+ }
1304
+ pre code {
1305
+ display: block;
1306
+ padding: 1rem;
1307
+ overflow-x: auto;
1308
+ }
1309
+ /* Apply background color to code blocks based on theme */
1310
+ pre.syntax-highlighter {
1311
+ background-color: var(--shiki-color-background) !important;
1312
+ }
1313
+ .dark-mode pre.syntax-highlighter {
1314
+ background-color: var(--shiki-color-background) !important;
1315
+ }
1316
+ code {
1317
+ padding: 0.2rem 0.4rem;
1318
+ border-radius: 3px;
1319
+ }
1320
+ h1, h2, h3, h4 {
1321
+ margin-top: 2rem;
1322
+ margin-bottom: 1rem;
1323
+ }
1324
+ h1 { color: #111; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
1325
+ h2 { color: #333; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.3rem; }
1326
+ h3 { color: #444; }
1327
+ img {
1328
+ max-width: 100%;
1329
+ border-radius: 4px;
1330
+ margin: 1rem 0;
1331
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1332
+ }
1333
+ blockquote {
1334
+ border-left: 4px solid #ddd;
1335
+ padding-left: 1rem;
1336
+ margin-left: 0;
1337
+ color: #555;
1338
+ background: #f9f9f9;
1339
+ padding: 0.5rem 1rem;
1340
+ margin: 1.5rem 0;
1341
+ }
1342
+ table {
1343
+ border-collapse: collapse;
1344
+ width: 100%;
1345
+ margin: 1.5rem 0;
1346
+ }
1347
+ th, td {
1348
+ border: 1px solid #ddd;
1349
+ padding: 0.5rem;
1350
+ }
1351
+ th {
1352
+ background: #f0f0f0;
1353
+ text-align: left;
1354
+ }
1355
+ tr:nth-child(even) {
1356
+ background-color: #f8f8f8;
1357
+ }
1358
+ a {
1359
+ color: #0066cc;
1360
+ text-decoration: none;
1361
+ }
1362
+ a:hover {
1363
+ text-decoration: underline;
1364
+ }
1365
+ hr {
1366
+ border: 0;
1367
+ border-top: 1px solid #eee;
1368
+ margin: 2rem 0;
1369
+ }
1370
+ .frontmatter {
1371
+ background: #f8f8f8;
1372
+ border-radius: 4px;
1373
+ padding: 1.5rem;
1374
+ margin-bottom: 2rem;
1375
+ font-size: 0.9rem;
1376
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1377
+ border-left: 4px solid #ddd;
1378
+ }
1379
+ .frontmatter-item {
1380
+ margin-bottom: 0.5rem;
1381
+ display: flex;
1382
+ }
1383
+ .frontmatter-label {
1384
+ font-weight: bold;
1385
+ min-width: 100px;
1386
+ color: #555;
1387
+ }
1388
+ /* Theme selector for code blocks */
1389
+ .theme-selector {
1390
+ margin: 1rem 0;
1391
+ padding: 0.5rem;
1392
+ background: #f8f8f8;
1393
+ border-radius: 4px;
1394
+ text-align: right;
1395
+ }
1396
+ select {
1397
+ padding: 0.25rem 0.5rem;
1398
+ border-radius: 3px;
1399
+ border: 1px solid #ddd;
1400
+ }
1401
+
1402
+ /* Dark mode body styles */
1403
+ body.dark-mode {
1404
+ background-color: #121212;
1405
+ color: #e1e4e8;
1406
+ }
1407
+
1408
+ body.dark-mode h1,
1409
+ body.dark-mode h2,
1410
+ body.dark-mode h3 {
1411
+ color: #e1e4e8;
1412
+ border-color: #2f363d;
1413
+ }
1414
+
1415
+ body.dark-mode .theme-selector {
1416
+ background: #2f363d;
1417
+ color: #e1e4e8;
1418
+ }
1419
+
1420
+ body.dark-mode select {
1421
+ background: #24292e;
1422
+ color: #e1e4e8;
1423
+ border-color: #444;
1424
+ }
1425
+
1426
+ body.dark-mode blockquote {
1427
+ background: #24292e;
1428
+ color: #e1e4e8;
1429
+ }
1430
+
1431
+ body.dark-mode .frontmatter {
1432
+ background: #24292e;
1433
+ }
1434
+
1435
+ body.dark-mode a {
1436
+ color: #58a6ff;
1437
+ }
1438
+ </style>
1439
+ </head>
1440
+ <body>
1441
+ ${Object.keys(data).length > 0 ? `
1442
+ <div class="frontmatter">
1443
+ <h3>Frontmatter</h3>
1444
+ ${Object.entries(data).map(([key, value]) => `
1445
+ <div class="frontmatter-item">
1446
+ <span class="frontmatter-label">${key}:</span>
1447
+ <span>${Array.isArray(value) ? value.join(", ") : value}</span>
1448
+ </div>`).join("")}
1449
+ </div>
1450
+ ` : ""}
1451
+
1452
+ <div class="theme-selector">
1453
+ Theme:
1454
+ <select id="themeSelector" onchange="changeTheme()">
1455
+ ${themeOptions}
1456
+ </select>
1457
+ </div>
1458
+
1459
+ ${content}
1460
+
1461
+ <script>
1462
+ function changeTheme() {
1463
+ const theme = document.getElementById('themeSelector').value;
1464
+
1465
+ // Toggle dark mode class based on theme
1466
+ if (theme.includes('dark') || theme.includes('night') || theme.includes('monokai') ||
1467
+ theme.includes('dracula') || theme.includes('nord') || theme.includes('material')) {
1468
+ document.body.classList.add('dark-mode');
1469
+ } else {
1470
+ document.body.classList.remove('dark-mode');
1471
+ }
1472
+ }
1473
+
1474
+ // Initialize theme on page load
1475
+ document.addEventListener('DOMContentLoaded', function() {
1476
+ changeTheme();
1477
+ });
1478
+ </script>
1479
+ </body>
1480
+ </html>
1481
+ `;
1482
+ const relativePath = path3.relative(commonDir, absolutePath);
1483
+ const routePath = `/${relativePath.replace(/\.md$/, "")}`;
1484
+ routes[routePath || "/"] = {
1485
+ filePath: absolutePath,
1486
+ content: htmlContent,
1487
+ fileType: "md"
1488
+ };
1489
+ } catch (error) {
1490
+ console.error(`${colors.red}Error processing Markdown file ${colors.bright}${filePath}${colors.reset}:`, error);
1491
+ continue;
1492
+ }
1493
+ } else {
1494
+ const result = await Bun.build({
1495
+ entrypoints: [absolutePath],
1496
+ outdir: outputDir,
1497
+ plugins: [plugin],
1498
+ define: {
1499
+ "process.env.NODE_ENV": '"development"'
1500
+ },
1501
+ ...options.stxOptions
1502
+ });
1503
+ if (!result.success) {
1504
+ console.error(`${colors.red}Build failed for ${colors.bright}${filePath}${colors.reset}:`, result.logs);
1505
+ continue;
1506
+ }
1507
+ const htmlOutput = result.outputs.find((o) => o.path.endsWith(".html"));
1508
+ if (!htmlOutput) {
1509
+ console.error(`${colors.red}No HTML output found for ${colors.bright}${filePath}${colors.reset}`);
1510
+ continue;
1511
+ }
1512
+ const htmlContent = await Bun.file(htmlOutput.path).text();
1513
+ const relativePath = path3.relative(commonDir, absolutePath);
1514
+ const routePath = `/${relativePath.replace(/\.stx$/, "")}`;
1515
+ routes[routePath || "/"] = {
1516
+ filePath: absolutePath,
1517
+ content: htmlContent,
1518
+ fileType: "stx"
1519
+ };
1520
+ }
1521
+ }
1522
+ if (Object.keys(routes).length === 0) {
1523
+ console.error(`${colors.red}No files were successfully processed${colors.reset}`);
1524
+ return false;
1525
+ }
1526
+ return true;
1527
+ } catch (error) {
1528
+ console.error(`${colors.red}Error processing files:${colors.reset}`, error);
1529
+ return false;
1530
+ }
1531
+ };
1532
+ const buildSuccess = await buildFiles();
1533
+ if (!buildSuccess) {
1534
+ return false;
1535
+ }
1536
+ let actualPort = port;
1537
+ try {
1538
+ actualPort = await findAvailablePort(port);
1539
+ if (actualPort !== port) {
1540
+ console.log(`${colors.yellow}Port ${port} is busy, using port ${actualPort} instead${colors.reset}`);
1541
+ }
1542
+ } catch {
1543
+ console.error(`${colors.red}Could not find an available port${colors.reset}`);
1544
+ return false;
1545
+ }
1546
+ console.log(`${colors.blue}Starting server on ${colors.cyan}http://localhost:${actualPort}/${colors.reset}...`);
1547
+ const server = serve({
1548
+ port: actualPort,
1549
+ fetch(request) {
1550
+ const url = new URL(request.url);
1551
+ let routeMatched = routes[url.pathname];
1552
+ if (!routeMatched && !url.pathname.endsWith("/")) {
1553
+ routeMatched = routes[`${url.pathname}/`];
1554
+ }
1555
+ if (!routeMatched && url.pathname !== "/" && routes["/"]) {
1556
+ routeMatched = routes["/"];
1557
+ }
1558
+ if (routeMatched) {
1559
+ return new Response(routeMatched.content, {
1560
+ headers: {
1561
+ "Content-Type": "text/html"
1562
+ }
1563
+ });
1564
+ }
1565
+ const requestedPath = path3.join(outputDir, url.pathname);
1566
+ if (fs2.existsSync(requestedPath) && fs2.statSync(requestedPath).isFile()) {
1567
+ const file = Bun.file(requestedPath);
1568
+ const ext = path3.extname(requestedPath).toLowerCase();
1569
+ let contentType = "text/plain";
1570
+ switch (ext) {
1571
+ case ".html":
1572
+ contentType = "text/html";
1573
+ break;
1574
+ case ".css":
1575
+ contentType = "text/css";
1576
+ break;
1577
+ case ".js":
1578
+ contentType = "text/javascript";
1579
+ break;
1580
+ case ".json":
1581
+ contentType = "application/json";
1582
+ break;
1583
+ case ".png":
1584
+ contentType = "image/png";
1585
+ break;
1586
+ case ".jpg":
1587
+ case ".jpeg":
1588
+ contentType = "image/jpeg";
1589
+ break;
1590
+ case ".gif":
1591
+ contentType = "image/gif";
1592
+ break;
1593
+ }
1594
+ return new Response(file, {
1595
+ headers: { "Content-Type": contentType }
1596
+ });
1597
+ }
1598
+ return new Response("Not Found", { status: 404 });
1599
+ },
1600
+ error(error) {
1601
+ return new Response(`<pre>${error}
1602
+ ${error.stack}</pre>`, {
1603
+ headers: {
1604
+ "Content-Type": "text/html"
1605
+ }
1606
+ });
1607
+ }
1608
+ });
1609
+ console.clear();
1610
+ console.log(`
1611
+ ${colors.blue}stx${colors.reset} ${colors.green}${process2.env.stx_VERSION || "v0.0.10"}${colors.reset} ${colors.dim}ready in ${Math.random() * 10 + 5 | 0}.${Math.random() * 90 + 10 | 0} ms${colors.reset}`);
1612
+ console.log(`
1613
+ ${colors.bright}\u2192 ${colors.cyan}http://localhost:${port}/${colors.reset}`);
1614
+ console.log(`
1615
+ ${colors.yellow}Routes:${colors.reset}`);
1616
+ const sortedRoutes = Object.entries(routes).sort(([pathA], [pathB]) => pathA.localeCompare(pathB)).map(([route, info]) => ({
1617
+ route: route === "/" ? "/" : route,
1618
+ filePath: path3.relative(process2.cwd(), info.filePath),
1619
+ fileType: info.fileType
1620
+ }));
1621
+ sortedRoutes.forEach((routeInfo, index) => {
1622
+ const isLast = index === sortedRoutes.length - 1;
1623
+ const prefix = isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1624
+ const fileTypeLabel = routeInfo.fileType === "md" ? `${colors.magenta}(markdown)${colors.reset}` : "";
1625
+ if (routeInfo.route === "/") {
1626
+ console.log(` ${colors.green}${prefix}/${colors.reset} \u2192 ${colors.bright}${routeInfo.filePath}${colors.reset} ${fileTypeLabel}`);
1627
+ } else {
1628
+ const routeParts = routeInfo.route.split("/");
1629
+ const lastPart = routeParts[routeParts.length - 1] || routeParts[routeParts.length - 2];
1630
+ const displayRoute = routeInfo.route === "/" ? "/" : `/${lastPart}`;
1631
+ let parentPath = routeParts.slice(0, -1).join("/");
1632
+ if (parentPath && !parentPath.startsWith("/"))
1633
+ parentPath = `/${parentPath}`;
1634
+ console.log(` ${colors.green}${prefix}${displayRoute}${colors.reset} \u2192 ${colors.bright}${routeInfo.filePath}${colors.reset} ${fileTypeLabel}`);
1635
+ }
1636
+ });
1637
+ console.log(`
1638
+ Press ${colors.cyan}h${colors.reset} + ${colors.cyan}Enter${colors.reset} to show shortcuts`);
1639
+ setupKeyboardShortcuts(server.url.toString(), () => {
1640
+ if (watch) {
1641
+ const watcher = fs2.watch(commonDir, { recursive: true });
1642
+ watcher.close();
1643
+ }
1644
+ server.stop();
1645
+ });
1646
+ if (watch) {
1647
+ console.log(`${colors.blue}Watching for changes...${colors.reset}`);
1648
+ const watcher = fs2.watch(commonDir, { recursive: true }, async (eventType, filename) => {
1649
+ if (!filename)
1650
+ return;
1651
+ if (filename.endsWith(".stx") || filename.endsWith(".js") || filename.endsWith(".ts") || filename.endsWith(".md")) {
1652
+ console.log(`${colors.yellow}File ${colors.bright}${filename}${colors.yellow} changed, rebuilding...${colors.reset}`);
1653
+ await buildFiles();
1654
+ }
1655
+ });
1656
+ process2.on("SIGINT", () => {
1657
+ watcher.close();
1658
+ server.stop();
1659
+ process2.exit(0);
1660
+ });
1661
+ }
1662
+ return true;
1663
+ }
1664
+ function findCommonDir(paths) {
1665
+ if (paths.length === 0)
1666
+ return "";
1667
+ if (paths.length === 1)
1668
+ return paths[0];
1669
+ const parts = paths.map((p) => p.split(path3.sep));
1670
+ const commonParts = [];
1671
+ for (let i = 0;i < parts[0].length; i++) {
1672
+ const part = parts[0][i];
1673
+ if (parts.every((p) => p[i] === part)) {
1674
+ commonParts.push(part);
1675
+ } else {
1676
+ break;
1677
+ }
1678
+ }
1679
+ return commonParts.join(path3.sep);
1680
+ }
1681
+ // src/docs.ts
1682
+ import fs3 from "fs";
1683
+ import path4 from "path";
1684
+ async function extractComponentProps(componentPath) {
1685
+ try {
1686
+ const content = await Bun.file(componentPath).text();
1687
+ const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
1688
+ if (!scriptMatch)
1689
+ return [];
1690
+ const scriptContent = scriptMatch[1];
1691
+ const props = [];
1692
+ const propRegex = /\/\*\*\s*([\s\S]*?)\s*\*\/\s*(?:(?:const|let|var)\s*)?(?:([\w$]+)\s*=|module\.exports\.([\w$]+)\s*=|\s*([\w$]+)\s*:)/g;
1693
+ let match;
1694
+ while ((match = propRegex.exec(scriptContent)) !== null) {
1695
+ const commentBlock = match[1];
1696
+ const propName = match[2] || match[3] || match[4];
1697
+ if (!propName)
1698
+ continue;
1699
+ const propDoc = { name: propName };
1700
+ const typeMatch = commentBlock.match(/@type\s+\{([^}]+)\}/i);
1701
+ if (typeMatch)
1702
+ propDoc.type = typeMatch[1].trim();
1703
+ const requiredMatch = commentBlock.match(/@required/i);
1704
+ if (requiredMatch)
1705
+ propDoc.required = true;
1706
+ const defaultMatch = commentBlock.match(/@default\s+(.+?)(?:\s+|$)/i);
1707
+ if (defaultMatch)
1708
+ propDoc.default = defaultMatch[1].trim();
1709
+ const descLines = commentBlock.split(`
1710
+ `).map((line) => line.trim().replace(/^\*\s*/, "")).filter((line) => !line.startsWith("@") && line.length > 0);
1711
+ propDoc.description = descLines.join(" ").trim();
1712
+ props.push(propDoc);
1713
+ }
1714
+ if (props.length === 0) {
1715
+ const exportsMatch = scriptContent.match(/module\.exports\s*=\s*\{([^}]+)\}/i);
1716
+ if (exportsMatch) {
1717
+ const exportsObject = exportsMatch[1];
1718
+ const propLines = exportsObject.split(",").map((line) => line.trim()).filter(Boolean);
1719
+ for (const propLine of propLines) {
1720
+ const [propName] = propLine.split(":").map((part) => part.trim());
1721
+ if (propName) {
1722
+ props.push({ name: propName });
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ return props;
1728
+ } catch (error) {
1729
+ console.error(`Error extracting props from ${componentPath}:`, error);
1730
+ return [];
1731
+ }
1732
+ }
1733
+ async function extractComponentDescription(componentPath) {
1734
+ try {
1735
+ const content = await Bun.file(componentPath).text();
1736
+ const commentMatch = content.match(/^\s*<!--\s*([\s\S]*?)\s*-->/) || content.match(/^\s*\/\*\*\s*([\s\S]*?)\s*\*\//);
1737
+ if (commentMatch) {
1738
+ return commentMatch[1].split(`
1739
+ `).map((line) => line.trim().replace(/^\*\s*/, "")).join(" ").trim();
1740
+ }
1741
+ return "";
1742
+ } catch {
1743
+ return "";
1744
+ }
1745
+ }
1746
+ async function generateComponentDoc(componentPath, isWebComponent = false, webComponentTag) {
1747
+ const name = path4.basename(componentPath, ".stx");
1748
+ const props = await extractComponentProps(componentPath);
1749
+ const description = await extractComponentDescription(componentPath);
1750
+ let example = "";
1751
+ if (isWebComponent && webComponentTag) {
1752
+ example = `<${webComponentTag}></${webComponentTag}>`;
1753
+ } else {
1754
+ const propsExample = props.map((prop) => {
1755
+ if (prop.type === "boolean")
1756
+ return `:${prop.name}="true"`;
1757
+ return `${prop.name}="value"`;
1758
+ }).join(`
1759
+ `);
1760
+ example = `<${name}
1761
+ ${propsExample}
1762
+ />`;
1763
+ }
1764
+ return {
1765
+ name,
1766
+ path: componentPath,
1767
+ description,
1768
+ props,
1769
+ example,
1770
+ isWebComponent,
1771
+ tag: webComponentTag
1772
+ };
1773
+ }
1774
+ async function findComponentFiles(componentsDir) {
1775
+ try {
1776
+ if (!await fileExists(componentsDir)) {
1777
+ console.warn(`Components directory does not exist: ${componentsDir}`);
1778
+ return [];
1779
+ }
1780
+ const entries = await fs3.promises.readdir(componentsDir, { withFileTypes: true });
1781
+ const componentFiles = [];
1782
+ for (const entry of entries) {
1783
+ const entryPath = path4.join(componentsDir, entry.name);
1784
+ if (entry.isDirectory()) {
1785
+ const subComponents = await findComponentFiles(entryPath);
1786
+ componentFiles.push(...subComponents);
1787
+ } else if (entry.isFile() && entry.name.endsWith(".stx")) {
1788
+ componentFiles.push(entryPath);
1789
+ }
1790
+ }
1791
+ return componentFiles;
1792
+ } catch (error) {
1793
+ console.error(`Error finding component files in ${componentsDir}:`, error);
1794
+ return [];
1795
+ }
1796
+ }
1797
+ async function generateComponentsDocs(componentsDir, webComponentsConfig) {
1798
+ try {
1799
+ if (!await fileExists(componentsDir)) {
1800
+ return [];
1801
+ }
1802
+ const componentFiles = await findComponentFiles(componentsDir);
1803
+ const componentDocs = [];
1804
+ const webComponentsMap = new Map;
1805
+ if (webComponentsConfig?.components?.length) {
1806
+ for (const component of webComponentsConfig.components) {
1807
+ webComponentsMap.set(component.file, component);
1808
+ }
1809
+ }
1810
+ for (const componentFile of componentFiles) {
1811
+ const webComponent = webComponentsMap.get(componentFile);
1812
+ const isWebComponent = !!webComponent;
1813
+ const webComponentTag = webComponent?.tag;
1814
+ const doc = await generateComponentDoc(componentFile, isWebComponent, webComponentTag);
1815
+ componentDocs.push(doc);
1816
+ }
1817
+ return componentDocs;
1818
+ } catch (error) {
1819
+ console.error(`Error generating component docs:`, error);
1820
+ return [];
1821
+ }
1822
+ }
1823
+ async function generateTemplatesDocs(templatesDir) {
1824
+ try {
1825
+ if (!await fileExists(templatesDir)) {
1826
+ return [];
1827
+ }
1828
+ const entries = await fs3.promises.readdir(templatesDir, { withFileTypes: true });
1829
+ const templateDocs = [];
1830
+ for (const entry of entries) {
1831
+ if (!entry.isFile() || !entry.name.endsWith(".stx"))
1832
+ continue;
1833
+ const templatePath = path4.join(templatesDir, entry.name);
1834
+ const content = await Bun.file(templatePath).text();
1835
+ const name = path4.basename(templatePath, ".stx");
1836
+ const descriptionMatch = content.match(/^\s*<!--\s*([\s\S]*?)\s*-->/) || content.match(/^\s*\/\*\*\s*([\s\S]*?)\s*\*\//);
1837
+ const description = descriptionMatch ? descriptionMatch[1].split(`
1838
+ `).map((line) => line.trim().replace(/^\*\s*/, "")).join(" ").trim() : "";
1839
+ const componentRegex = /@component\(\s*['"]([^'"]+)['"]/g;
1840
+ const componentTags = /<([A-Z][a-zA-Z0-9]*|[a-z]+-[a-z0-9-]+)(?:\s[^>]*)?\/?>|\{\{\s*slot\s*\}\}/g;
1841
+ const components = new Set;
1842
+ let match;
1843
+ while ((match = componentRegex.exec(content)) !== null) {
1844
+ components.add(match[1]);
1845
+ }
1846
+ while ((match = componentTags.exec(content)) !== null) {
1847
+ if (match[1] && match[1] !== "slot") {
1848
+ components.add(match[1]);
1849
+ }
1850
+ }
1851
+ const directiveRegex = /@([a-z]+)(?:\s*\(|\s+|$)/g;
1852
+ const directives = new Set;
1853
+ while ((match = directiveRegex.exec(content)) !== null) {
1854
+ directives.add(match[1]);
1855
+ }
1856
+ templateDocs.push({
1857
+ name,
1858
+ path: templatePath,
1859
+ description,
1860
+ components: [...components],
1861
+ directives: [...directives]
1862
+ });
1863
+ }
1864
+ return templateDocs;
1865
+ } catch (error) {
1866
+ console.error(`Error generating template docs:`, error);
1867
+ return [];
1868
+ }
1869
+ }
1870
+ async function generateDirectivesDocs(customDirectives = []) {
1871
+ try {
1872
+ const directiveDocs = [
1873
+ {
1874
+ name: "if",
1875
+ description: "Conditionally render content based on a condition",
1876
+ hasEndTag: true,
1877
+ example: `@if(user.isLoggedIn)
1878
+ <p>Welcome, {{ user.name }}!</p>
1879
+ @endif`
1880
+ },
1881
+ {
1882
+ name: "else",
1883
+ description: "Provides an alternative if a condition is not met",
1884
+ hasEndTag: false,
1885
+ example: `@if(user.isLoggedIn)
1886
+ <p>Welcome back!</p>
1887
+ @else
1888
+ <p>Please log in</p>
1889
+ @endif`
1890
+ },
1891
+ {
1892
+ name: "elseif",
1893
+ description: "Provides an alternative condition",
1894
+ hasEndTag: false,
1895
+ example: `@if(score > 90)
1896
+ <p>A</p>
1897
+ @elseif(score > 80)
1898
+ <p>B</p>
1899
+ @elseif(score > 70)
1900
+ <p>C</p>
1901
+ @endif`
1902
+ },
1903
+ {
1904
+ name: "unless",
1905
+ description: "Conditionally render content if a condition is false",
1906
+ hasEndTag: true,
1907
+ example: `@unless(user.isLoggedIn)
1908
+ <p>Please log in</p>
1909
+ @endunless`
1910
+ },
1911
+ {
1912
+ name: "for",
1913
+ description: "Loop through an array or object",
1914
+ hasEndTag: true,
1915
+ example: `@for(item of items)
1916
+ <li>{{ item.name }}</li>
1917
+ @endfor`
1918
+ },
1919
+ {
1920
+ name: "while",
1921
+ description: "Loop while a condition is true",
1922
+ hasEndTag: true,
1923
+ example: `@while(page < totalPages)
1924
+ <p>Page {{ page }}</p>
1925
+ @endwhile`
1926
+ },
1927
+ {
1928
+ name: "component",
1929
+ description: "Include a component in the template",
1930
+ hasEndTag: false,
1931
+ example: '@component("alert", { type: "warning", title: "Warning", message: "This is a warning" })'
1932
+ },
1933
+ {
1934
+ name: "include",
1935
+ description: "Include a partial template",
1936
+ hasEndTag: false,
1937
+ example: '@include("partials/header")'
1938
+ },
1939
+ {
1940
+ name: "raw",
1941
+ description: "Display content without processing expressions",
1942
+ hasEndTag: true,
1943
+ example: `@raw
1944
+ {{ This will be displayed as-is }}
1945
+ @endraw`
1946
+ },
1947
+ {
1948
+ name: "translate",
1949
+ description: "Translate a string using the i18n system",
1950
+ hasEndTag: false,
1951
+ example: '@translate("welcome.message", { name: user.name })'
1952
+ },
1953
+ {
1954
+ name: "t",
1955
+ description: "Short alias for translate directive",
1956
+ hasEndTag: false,
1957
+ example: '@t("welcome.message", { name: user.name })'
1958
+ }
1959
+ ];
1960
+ for (const directive of customDirectives) {
1961
+ directiveDocs.push({
1962
+ name: directive.name,
1963
+ description: directive.description || "",
1964
+ hasEndTag: directive.hasEndTag || false,
1965
+ example: ""
1966
+ });
1967
+ }
1968
+ return directiveDocs;
1969
+ } catch (error) {
1970
+ console.error(`Error generating directive docs:`, error);
1971
+ return [];
1972
+ }
1973
+ }
1974
+ function formatDocsAsMarkdown(componentDocs = [], templateDocs = [], directiveDocs = [], extraContent) {
1975
+ let markdown = `# stx Documentation
1976
+
1977
+ `;
1978
+ if (extraContent) {
1979
+ markdown += `${extraContent}
1980
+
1981
+ `;
1982
+ }
1983
+ if (componentDocs.length > 0) {
1984
+ markdown += `## Components
1985
+
1986
+ `;
1987
+ for (const doc of componentDocs) {
1988
+ markdown += `### ${doc.name}
1989
+
1990
+ `;
1991
+ if (doc.description) {
1992
+ markdown += `${doc.description}
1993
+
1994
+ `;
1995
+ }
1996
+ if (doc.isWebComponent) {
1997
+ markdown += `**Web Component Tag:** \`${doc.tag}\`
1998
+
1999
+ `;
2000
+ }
2001
+ if (doc.props.length > 0) {
2002
+ markdown += `#### Properties
2003
+
2004
+ `;
2005
+ markdown += `| Name | Type | Required | Default | Description |
2006
+ `;
2007
+ markdown += `| ---- | ---- | -------- | ------- | ----------- |
2008
+ `;
2009
+ for (const prop of doc.props) {
2010
+ markdown += `| ${prop.name} | ${prop.type || "any"} | ${prop.required ? "Yes" : "No"} | ${prop.default || "-"} | ${prop.description || "-"} |
2011
+ `;
2012
+ }
2013
+ markdown += `
2014
+ `;
2015
+ }
2016
+ if (doc.example) {
2017
+ markdown += `#### Example
2018
+
2019
+ `;
2020
+ markdown += "```html\n";
2021
+ markdown += doc.example;
2022
+ markdown += "\n```\n\n";
82
2023
  }
83
2024
  }
84
- build.onLoad({ filter: /\.stx$/ }, async ({ path: filePath }) => {
85
- try {
86
- const dependencies = new Set;
87
- if (options.cache && options.cachePath) {
88
- const cachedOutput = await checkCache(filePath, options);
89
- if (cachedOutput) {
90
- if (options.debug) {
91
- console.log(`Using cached version of ${filePath}`);
92
- }
93
- return {
94
- contents: cachedOutput,
95
- loader: "html"
96
- };
97
- }
2025
+ }
2026
+ if (templateDocs.length > 0) {
2027
+ markdown += `## Templates
2028
+
2029
+ `;
2030
+ for (const doc of templateDocs) {
2031
+ markdown += `### ${doc.name}
2032
+
2033
+ `;
2034
+ if (doc.description) {
2035
+ markdown += `${doc.description}
2036
+
2037
+ `;
2038
+ }
2039
+ if (doc.components && doc.components.length > 0) {
2040
+ markdown += `**Components Used:** ${doc.components.join(", ")}
2041
+
2042
+ `;
2043
+ }
2044
+ if (doc.directives && doc.directives.length > 0) {
2045
+ markdown += `**Directives Used:** ${doc.directives.join(", ")}
2046
+
2047
+ `;
2048
+ }
2049
+ }
2050
+ }
2051
+ if (directiveDocs.length > 0) {
2052
+ markdown += `## Directives
2053
+
2054
+ `;
2055
+ markdown += `| Directive | Description | Has End Tag |
2056
+ `;
2057
+ markdown += `| --------- | ----------- | ----------- |
2058
+ `;
2059
+ for (const doc of directiveDocs) {
2060
+ markdown += `| @${doc.name} | ${doc.description || "-"} | ${doc.hasEndTag ? "Yes" : "No"} |
2061
+ `;
2062
+ }
2063
+ markdown += `
2064
+ `;
2065
+ for (const doc of directiveDocs) {
2066
+ if (doc.example) {
2067
+ markdown += `### @${doc.name}
2068
+
2069
+ `;
2070
+ markdown += `${doc.description || ""}
2071
+
2072
+ `;
2073
+ markdown += `#### Example
2074
+
2075
+ `;
2076
+ markdown += "```html\n";
2077
+ markdown += doc.example;
2078
+ markdown += "\n```\n\n";
2079
+ }
2080
+ }
2081
+ }
2082
+ return markdown;
2083
+ }
2084
+ function formatDocsAsHtml(componentDocs = [], templateDocs = [], directiveDocs = [], extraContent) {
2085
+ let html = `<!DOCTYPE html>
2086
+ <html lang="en">
2087
+ <head>
2088
+ <meta charset="UTF-8">
2089
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2090
+ <title>stx Documentation</title>
2091
+ <style>
2092
+ body {
2093
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
2094
+ line-height: 1.6;
2095
+ color: #333;
2096
+ max-width: 1200px;
2097
+ margin: 0 auto;
2098
+ padding: 2rem;
2099
+ }
2100
+ h1, h2, h3, h4 { margin-top: 2rem; }
2101
+ table {
2102
+ border-collapse: collapse;
2103
+ width: 100%;
2104
+ margin: 1rem 0;
2105
+ }
2106
+ th, td {
2107
+ text-align: left;
2108
+ padding: 0.5rem;
2109
+ border-bottom: 1px solid #ddd;
2110
+ }
2111
+ th { border-bottom: 2px solid #ddd; }
2112
+ pre {
2113
+ background: #f5f5f5;
2114
+ padding: 1rem;
2115
+ border-radius: 4px;
2116
+ overflow-x: auto;
2117
+ }
2118
+ code {
2119
+ background: #f5f5f5;
2120
+ padding: 0.2rem 0.4rem;
2121
+ border-radius: 4px;
2122
+ font-size: 0.9em;
2123
+ }
2124
+ </style>
2125
+ </head>
2126
+ <body>
2127
+ <h1>stx Documentation</h1>
2128
+ `;
2129
+ if (extraContent) {
2130
+ html += `<div>${extraContent}</div>`;
2131
+ }
2132
+ if (componentDocs.length > 0) {
2133
+ html += `<h2>Components</h2>`;
2134
+ for (const doc of componentDocs) {
2135
+ html += `<h3>${doc.name}</h3>`;
2136
+ if (doc.description) {
2137
+ html += `<p>${doc.description}</p>`;
2138
+ }
2139
+ if (doc.isWebComponent) {
2140
+ html += `<p><strong>Web Component Tag:</strong> <code>${doc.tag}</code></p>`;
2141
+ }
2142
+ if (doc.props.length > 0) {
2143
+ html += `<h4>Properties</h4>`;
2144
+ html += `<table>
2145
+ <thead>
2146
+ <tr>
2147
+ <th>Name</th>
2148
+ <th>Type</th>
2149
+ <th>Required</th>
2150
+ <th>Default</th>
2151
+ <th>Description</th>
2152
+ </tr>
2153
+ </thead>
2154
+ <tbody>`;
2155
+ for (const prop of doc.props) {
2156
+ html += `<tr>
2157
+ <td>${prop.name}</td>
2158
+ <td>${prop.type || "any"}</td>
2159
+ <td>${prop.required ? "Yes" : "No"}</td>
2160
+ <td>${prop.default || "-"}</td>
2161
+ <td>${prop.description || "-"}</td>
2162
+ </tr>`;
98
2163
  }
99
- const content = await Bun.file(filePath).text();
100
- const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
101
- const scriptContent = scriptMatch ? scriptMatch[1] : "";
102
- const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, "");
103
- const context = {
104
- __filename: filePath,
105
- __dirname: path.dirname(filePath),
106
- __stx: {
107
- webComponentsPath,
108
- builtComponents
109
- }
110
- };
111
- await extractVariables(scriptContent, context, filePath);
112
- let output = templateContent;
113
- output = await processDirectives(output, context, filePath, options, dependencies);
114
- dependencies.forEach((dep) => allDependencies.add(dep));
115
- if (options.cache && options.cachePath) {
116
- await cacheTemplate(filePath, output, dependencies, options);
117
- if (options.debug) {
118
- console.log(`Cached template ${filePath} with ${dependencies.size} dependencies`);
119
- }
2164
+ html += `</tbody></table>`;
2165
+ }
2166
+ if (doc.example) {
2167
+ html += `<h4>Example</h4>`;
2168
+ html += `<pre><code>${doc.example.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;")}</code></pre>`;
2169
+ }
2170
+ }
2171
+ }
2172
+ if (templateDocs.length > 0) {
2173
+ html += `<h2>Templates</h2>`;
2174
+ for (const doc of templateDocs) {
2175
+ html += `<h3>${doc.name}</h3>`;
2176
+ if (doc.description) {
2177
+ html += `<p>${doc.description}</p>`;
2178
+ }
2179
+ if (doc.components && doc.components.length > 0) {
2180
+ html += `<p><strong>Components Used:</strong> ${doc.components.join(", ")}</p>`;
2181
+ }
2182
+ if (doc.directives && doc.directives.length > 0) {
2183
+ html += `<p><strong>Directives Used:</strong> ${doc.directives.join(", ")}</p>`;
2184
+ }
2185
+ }
2186
+ }
2187
+ if (directiveDocs.length > 0) {
2188
+ html += `<h2>Directives</h2>`;
2189
+ html += `<table>
2190
+ <thead>
2191
+ <tr>
2192
+ <th>Directive</th>
2193
+ <th>Description</th>
2194
+ <th>Has End Tag</th>
2195
+ </tr>
2196
+ </thead>
2197
+ <tbody>`;
2198
+ for (const doc of directiveDocs) {
2199
+ html += `<tr>
2200
+ <td>@${doc.name}</td>
2201
+ <td>${doc.description || "-"}</td>
2202
+ <td>${doc.hasEndTag ? "Yes" : "No"}</td>
2203
+ </tr>`;
2204
+ }
2205
+ html += `</tbody></table>`;
2206
+ for (const doc of directiveDocs) {
2207
+ if (doc.example) {
2208
+ html += `<h3>@${doc.name}</h3>`;
2209
+ html += `<p>${doc.description || ""}</p>`;
2210
+ html += `<h4>Example</h4>`;
2211
+ html += `<pre><code>${doc.example.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;")}</code></pre>`;
2212
+ }
2213
+ }
2214
+ }
2215
+ html += `</body></html>`;
2216
+ return html;
2217
+ }
2218
+ function formatDocsAsJson(componentDocs = [], templateDocs = [], directiveDocs = [], extraContent) {
2219
+ const json = {
2220
+ components: componentDocs,
2221
+ templates: templateDocs,
2222
+ directives: directiveDocs,
2223
+ extraContent: extraContent || ""
2224
+ };
2225
+ return JSON.stringify(json, null, 2);
2226
+ }
2227
+ async function generateDocs(options) {
2228
+ try {
2229
+ const { componentsDir, templatesDir, webComponentsConfig, customDirectives, config: docConfig } = options;
2230
+ if (!docConfig.enabled) {
2231
+ return false;
2232
+ }
2233
+ const outputDir = docConfig.outputDir || "docs";
2234
+ await fs3.promises.mkdir(outputDir, { recursive: true });
2235
+ console.log(`Generating documentation...`);
2236
+ if (componentsDir)
2237
+ console.log(`Components directory: ${componentsDir}`);
2238
+ if (templatesDir)
2239
+ console.log(`Templates directory: ${templatesDir}`);
2240
+ console.log(`Output directory: ${outputDir}`);
2241
+ let componentDocs = [];
2242
+ if (docConfig.components !== false && componentsDir) {
2243
+ console.log(`Generating component documentation...`);
2244
+ componentDocs = await generateComponentsDocs(componentsDir, webComponentsConfig);
2245
+ console.log(`Found ${componentDocs.length} components`);
2246
+ }
2247
+ let templateDocs = [];
2248
+ if (docConfig.templates !== false && templatesDir) {
2249
+ console.log(`Generating template documentation...`);
2250
+ templateDocs = await generateTemplatesDocs(templatesDir);
2251
+ console.log(`Found ${templateDocs.length} templates`);
2252
+ }
2253
+ let directiveDocs = [];
2254
+ if (docConfig.directives !== false) {
2255
+ console.log(`Generating directive documentation...`);
2256
+ directiveDocs = await generateDirectivesDocs(customDirectives);
2257
+ console.log(`Found ${directiveDocs.length} directives`);
2258
+ }
2259
+ const format = docConfig.format || "markdown";
2260
+ const extraContent = docConfig.extraContent;
2261
+ let docContent = "";
2262
+ let extension = "";
2263
+ switch (format) {
2264
+ case "markdown":
2265
+ docContent = formatDocsAsMarkdown(componentDocs, templateDocs, directiveDocs, extraContent);
2266
+ extension = "md";
2267
+ break;
2268
+ case "html":
2269
+ docContent = formatDocsAsHtml(componentDocs, templateDocs, directiveDocs, extraContent);
2270
+ extension = "html";
2271
+ break;
2272
+ case "json":
2273
+ docContent = formatDocsAsJson(componentDocs, templateDocs, directiveDocs, extraContent);
2274
+ extension = "json";
2275
+ break;
2276
+ default:
2277
+ throw new Error(`Unsupported documentation format: ${format}`);
2278
+ }
2279
+ const outputPath = path4.join(outputDir, `stx-docs.${extension}`);
2280
+ await Bun.write(outputPath, docContent);
2281
+ console.log(`Documentation generated: ${outputPath}`);
2282
+ return true;
2283
+ } catch (error) {
2284
+ console.error("Error generating documentation:", error);
2285
+ return false;
2286
+ }
2287
+ }
2288
+ async function docsCommand(options) {
2289
+ const docsConfig = {
2290
+ enabled: true,
2291
+ outputDir: options.output || "docs",
2292
+ format: options.format || "markdown",
2293
+ components: options.components !== false,
2294
+ templates: options.templates !== false,
2295
+ directives: options.directives !== false,
2296
+ extraContent: options.extraContent
2297
+ };
2298
+ const componentsDir = options.componentsDir || config.componentsDir;
2299
+ const templatesDir = options.templatesDir || ".";
2300
+ return generateDocs({
2301
+ componentsDir,
2302
+ templatesDir,
2303
+ webComponentsConfig: config.webComponents,
2304
+ customDirectives: config.customDirectives,
2305
+ config: docsConfig
2306
+ });
2307
+ }
2308
+ // src/formatter.ts
2309
+ var DEFAULT_OPTIONS = {
2310
+ indentSize: 2,
2311
+ useTabs: false,
2312
+ maxLineLength: 120,
2313
+ normalizeWhitespace: true,
2314
+ sortAttributes: false,
2315
+ trimTrailingWhitespace: true
2316
+ };
2317
+ function formatStxContent(content, options = {}) {
2318
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2319
+ if (content.trim() === "") {
2320
+ return `
2321
+ `;
2322
+ }
2323
+ let formatted = content;
2324
+ if (opts.trimTrailingWhitespace) {
2325
+ formatted = formatted.replace(/[ \t]+$/gm, "");
2326
+ }
2327
+ formatted = formatScriptTags(formatted, opts);
2328
+ formatted = formatHtml(formatted, opts);
2329
+ formatted = formatAttributes(formatted, opts);
2330
+ formatted = formatStxDirectives(formatted, opts);
2331
+ if (opts.trimTrailingWhitespace) {
2332
+ formatted = formatted.replace(/[ \t]+$/gm, "");
2333
+ formatted = formatted.replace(/(\S)[ \t]+(<\/[^>]+>)/g, "$1$2");
2334
+ }
2335
+ formatted = formatted.replace(/\r\n/g, `
2336
+ `).replace(/\r/g, `
2337
+ `);
2338
+ if (!formatted.endsWith(`
2339
+ `)) {
2340
+ formatted += `
2341
+ `;
2342
+ }
2343
+ return formatted;
2344
+ }
2345
+ function formatScriptTags(content, options) {
2346
+ return content.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (match, scriptContent) => {
2347
+ const trimmed = scriptContent.trim();
2348
+ if (trimmed && !trimmed.includes(`
2349
+ `) && trimmed.length < 80) {
2350
+ return match.replace(scriptContent, trimmed);
2351
+ }
2352
+ const lines = scriptContent.split(`
2353
+ `);
2354
+ const formattedLines = lines.map((line, index) => {
2355
+ if (index === 0 && line.trim() === "")
2356
+ return "";
2357
+ if (index === lines.length - 1 && line.trim() === "")
2358
+ return "";
2359
+ const lineTrimmed = line.trim();
2360
+ if (lineTrimmed === "")
2361
+ return "";
2362
+ const indent = options.useTabs ? "\t" : " ".repeat(options.indentSize);
2363
+ return `${indent}${lineTrimmed}`;
2364
+ }).filter((line, index, arr) => {
2365
+ if (index === 0 || index === arr.length - 1)
2366
+ return line !== "";
2367
+ return true;
2368
+ });
2369
+ const formattedScript = formattedLines.length > 0 ? `
2370
+ ${formattedLines.join(`
2371
+ `)}
2372
+ ` : "";
2373
+ return match.replace(scriptContent, formattedScript);
2374
+ });
2375
+ }
2376
+ function formatHtml(content, options) {
2377
+ let preprocessed = content;
2378
+ const whitespaceSensitivePlaceholders = [];
2379
+ const whitespaceTags = ["pre", "code", "textarea", "style"];
2380
+ for (const tag of whitespaceTags) {
2381
+ const regex = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "gi");
2382
+ preprocessed = preprocessed.replace(regex, (match) => {
2383
+ const placeholder = `__WHITESPACE_TAG_${whitespaceSensitivePlaceholders.length}__`;
2384
+ whitespaceSensitivePlaceholders.push(match);
2385
+ return placeholder;
2386
+ });
2387
+ }
2388
+ const emptyTagPlaceholders = [];
2389
+ preprocessed = preprocessed.replace(/<(\w+)([^>]*)><\/\1>/g, (match) => {
2390
+ const placeholder = `__EMPTY_TAG_${emptyTagPlaceholders.length}__`;
2391
+ emptyTagPlaceholders.push(match);
2392
+ return placeholder;
2393
+ });
2394
+ preprocessed = preprocessed.replace(/<(\w+)([^>]*?)(@\w+\([^)]*\))([^>]*?)(@end\w+)([^>]*)>/g, (match, tagName, beforeDirective, openDirective, betweenDirectives, closeDirective, afterDirective) => {
2395
+ return `<${tagName}${beforeDirective}
2396
+ ${openDirective}
2397
+ ${betweenDirectives.trim()}
2398
+ ${closeDirective}${afterDirective}>`;
2399
+ });
2400
+ preprocessed = preprocessed.replace(/>(\s*)</g, (match, whitespace, offset, string) => {
2401
+ if (whitespace.includes(`
2402
+ `)) {
2403
+ return match;
2404
+ }
2405
+ const before = string.substring(Math.max(0, offset - 50), offset + 1);
2406
+ const after = string.substring(offset + match.length - 1, Math.min(string.length, offset + match.length + 50));
2407
+ const closingTagMatch = before.match(/<\/(\w+)>$/);
2408
+ const openingTagMatch = after.match(/^<(\w+)/);
2409
+ const inlineTags = ["span", "a", "strong", "em", "b", "i", "u", "small", "mark", "del", "ins", "sub", "sup", "code", "kbd", "samp", "var", "abbr", "cite", "q", "dfn", "time"];
2410
+ if (closingTagMatch && openingTagMatch) {
2411
+ const closingTag = closingTagMatch[1].toLowerCase();
2412
+ const openingTag = openingTagMatch[1].toLowerCase();
2413
+ if (inlineTags.includes(closingTag) && inlineTags.includes(openingTag)) {
2414
+ const beforeMatch = string.substring(0, offset);
2415
+ const lastNewline = beforeMatch.lastIndexOf(`
2416
+ `);
2417
+ const lineStart = lastNewline + 1;
2418
+ const afterMatch = string.substring(offset + match.length);
2419
+ const nextNewline = afterMatch.indexOf(`
2420
+ `);
2421
+ const lineEnd = nextNewline === -1 ? string.length : offset + match.length + nextNewline;
2422
+ const currentLine = string.substring(lineStart, lineEnd);
2423
+ const withoutTags = currentLine.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
2424
+ if (withoutTags.length > 0) {
2425
+ return match;
120
2426
  }
121
- return {
122
- contents: output,
123
- loader: "html"
124
- };
125
- } catch (error) {
126
- console.error("STX Plugin Error:", error);
127
- return {
128
- contents: `<!DOCTYPE html><html><body><h1>STX Rendering Error</h1><pre>${error.message || String(error)}</pre></body></html>`,
129
- loader: "html"
130
- };
131
2427
  }
2428
+ }
2429
+ return `>
2430
+ <`;
2431
+ });
2432
+ preprocessed = preprocessed.replace(/>(\s*)@/g, (match, whitespace) => {
2433
+ if (whitespace.includes(`
2434
+ `)) {
2435
+ return match;
2436
+ }
2437
+ return `>
2438
+ @`;
2439
+ });
2440
+ preprocessed = preprocessed.replace(/(@end\w+)(\s*)</g, (match, directive, whitespace) => {
2441
+ if (whitespace.includes(`
2442
+ `)) {
2443
+ return match;
2444
+ }
2445
+ return `${directive}
2446
+ <`;
2447
+ });
2448
+ preprocessed = preprocessed.replace(/\)(\s*)@/g, (match, whitespace) => {
2449
+ if (whitespace.includes(`
2450
+ `)) {
2451
+ return match;
2452
+ }
2453
+ return `)
2454
+ @`;
2455
+ });
2456
+ preprocessed = preprocessed.replace(/\)(\s*)</g, (match, whitespace) => {
2457
+ if (whitespace.includes(`
2458
+ `)) {
2459
+ return match;
2460
+ }
2461
+ return `)
2462
+ <`;
2463
+ });
2464
+ preprocessed = preprocessed.replace(/(@end\w+)(\s*)@/g, (match, directive, whitespace) => {
2465
+ if (whitespace.includes(`
2466
+ `)) {
2467
+ return match;
2468
+ }
2469
+ return `${directive}
2470
+ @`;
2471
+ });
2472
+ preprocessed = preprocessed.replace(/(@\w+)(?!\s*\()(\s*)</g, (match, directive, whitespace) => {
2473
+ if (whitespace.includes(`
2474
+ `)) {
2475
+ return match;
2476
+ }
2477
+ return `${directive}
2478
+ <`;
2479
+ });
2480
+ const lines = preprocessed.split(`
2481
+ `);
2482
+ const formattedLines = [];
2483
+ let indentLevel = 0;
2484
+ const indent = options.useTabs ? "\t" : " ".repeat(options.indentSize);
2485
+ for (let i = 0;i < lines.length; i++) {
2486
+ const line = lines[i].trim();
2487
+ if (line === "") {
2488
+ formattedLines.push("");
2489
+ continue;
2490
+ }
2491
+ if (line.startsWith("</") || line.startsWith("@end")) {
2492
+ indentLevel = Math.max(0, indentLevel - 1);
2493
+ }
2494
+ const indentedLine = indentLevel > 0 ? indent.repeat(indentLevel) + line : line;
2495
+ formattedLines.push(indentedLine);
2496
+ if (line.startsWith("<") && !line.includes("/>") && !line.startsWith("</")) {
2497
+ const hasMatchingClosingTag = line.match(/<(\w+)[^>]*>.*<\/\1>/);
2498
+ if (!line.includes("<!") && !isSelfClosingTag(line) && !hasMatchingClosingTag) {
2499
+ indentLevel++;
2500
+ }
2501
+ }
2502
+ if (line.startsWith("@") && isOpeningDirective(line)) {
2503
+ indentLevel++;
2504
+ }
2505
+ }
2506
+ let result = formattedLines.join(`
2507
+ `);
2508
+ whitespaceSensitivePlaceholders.forEach((tag, index) => {
2509
+ result = result.replace(`__WHITESPACE_TAG_${index}__`, tag);
2510
+ });
2511
+ emptyTagPlaceholders.forEach((tag, index) => {
2512
+ result = result.replace(`__EMPTY_TAG_${index}__`, tag);
2513
+ });
2514
+ return result;
2515
+ }
2516
+ function formatStxDirectives(content, options) {
2517
+ content = content.replace(/@(if|elseif|foreach|for|while)\s*\(\s*([^)]+)\s*\)/g, (match, directive, condition) => {
2518
+ const normalizedCondition = condition.trim().replace(/\s+/g, " ");
2519
+ return `@${directive}(${normalizedCondition})`;
2520
+ });
2521
+ content = content.replace(/@(csrf|method)\s*\(\s*([^)]*)\s*\)/g, (match, directive, param) => {
2522
+ if (param.trim() === "") {
2523
+ return `@${directive}`;
2524
+ }
2525
+ return `@${directive}(${param.trim()})`;
2526
+ });
2527
+ if (options.normalizeWhitespace) {
2528
+ content = content.replace(/\{\{\s*([^}]+)\s*\}\}/g, (match, expression) => {
2529
+ return `{{ ${expression.trim()} }}`;
2530
+ });
2531
+ content = content.replace(/\{!!\s*([^!]+)\s*!!\}/g, (match, expression) => {
2532
+ return `{!! ${expression.trim()} !!}`;
132
2533
  });
133
2534
  }
134
- };
135
- // src/caching.ts
136
- import fs from "fs";
137
- import path2 from "path";
138
- var templateCache = new Map;
139
- async function checkCache(filePath, options) {
2535
+ return content;
2536
+ }
2537
+ function isSelfClosingTag(line) {
2538
+ const selfClosingTags = ["br", "hr", "img", "input", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr"];
2539
+ if (line.includes("/>"))
2540
+ return true;
2541
+ const tagMatch = line.match(/<(\w+)/);
2542
+ if (tagMatch) {
2543
+ const tagName = tagMatch[1].toLowerCase();
2544
+ return selfClosingTags.includes(tagName);
2545
+ }
2546
+ return false;
2547
+ }
2548
+ function isOpeningDirective(line) {
2549
+ const blockDirectives = ["if", "unless", "foreach", "for", "while", "section", "push", "component", "slot", "markdown", "wrap", "error"];
2550
+ for (const directive of blockDirectives) {
2551
+ if (line.startsWith(`@${directive}`)) {
2552
+ return !line.includes(`@end${directive}`);
2553
+ }
2554
+ }
2555
+ return false;
2556
+ }
2557
+ function formatAttributes(content, options) {
2558
+ if (!options.sortAttributes)
2559
+ return content;
2560
+ return content.replace(/<(\w+)([^>]*)>/g, (match, tagName, attributes) => {
2561
+ if (!attributes.trim())
2562
+ return match;
2563
+ if (attributes.includes("<") || attributes.includes(`
2564
+ `) || attributes.includes("@")) {
2565
+ return match;
2566
+ }
2567
+ const attrRegex = /(\w+)(?:=("[^"]*"|'[^']*'|[^\s>]+))?/g;
2568
+ const attrs = [];
2569
+ let attrMatch;
2570
+ while ((attrMatch = attrRegex.exec(attributes)) !== null) {
2571
+ attrs.push({
2572
+ name: attrMatch[1],
2573
+ value: attrMatch[2]
2574
+ });
2575
+ }
2576
+ attrs.sort((a, b) => {
2577
+ if (a.name === "id")
2578
+ return -1;
2579
+ if (b.name === "id")
2580
+ return 1;
2581
+ if (a.name === "class")
2582
+ return -1;
2583
+ if (b.name === "class")
2584
+ return 1;
2585
+ return a.name.localeCompare(b.name);
2586
+ });
2587
+ const formattedAttrs = attrs.map((attr) => attr.value ? `${attr.name}=${attr.value}` : attr.name).join(" ");
2588
+ return `<${tagName}${formattedAttrs ? ` ${formattedAttrs}` : ""}>`;
2589
+ });
2590
+ }
2591
+ // src/init.ts
2592
+ import fs4 from "fs";
2593
+ import path5 from "path";
2594
+ import process3 from "process";
2595
+ async function initFile(fileName = "index.stx", options = {}) {
140
2596
  try {
141
- const cachePath = path2.resolve(options.cachePath);
142
- const cacheFile = path2.join(cachePath, `${hashFilePath(filePath)}.html`);
143
- const metaFile = path2.join(cachePath, `${hashFilePath(filePath)}.meta.json`);
144
- if (!await fileExists(cacheFile) || !await fileExists(metaFile))
145
- return null;
146
- const metaContent = await Bun.file(metaFile).text();
147
- const meta = JSON.parse(metaContent);
148
- if (meta.cacheVersion !== options.cacheVersion)
149
- return null;
150
- const stats = await fs.promises.stat(filePath);
151
- if (stats.mtime.getTime() > meta.mtime)
152
- return null;
153
- for (const dep of meta.dependencies) {
154
- if (await fileExists(dep)) {
155
- const depStats = await fs.promises.stat(dep);
156
- if (depStats.mtime.getTime() > meta.mtime)
157
- return null;
158
- } else {
159
- return null;
2597
+ const force = options.force || false;
2598
+ const filePath = path5.resolve(process3.cwd(), fileName);
2599
+ if (fs4.existsSync(filePath)) {
2600
+ if (!force) {
2601
+ throw new Error(`File ${fileName} already exists. Use --force to overwrite.`);
160
2602
  }
2603
+ console.warn(`File ${fileName} already exists. Overwriting...`);
161
2604
  }
162
- return await Bun.file(cacheFile).text();
163
- } catch (err) {
164
- console.warn(`Cache error for ${filePath}:`, err);
165
- return null;
2605
+ const dirPath = path5.dirname(filePath);
2606
+ if (!fs4.existsSync(dirPath)) {
2607
+ fs4.mkdirSync(dirPath, { recursive: true });
2608
+ }
2609
+ let templateContent;
2610
+ if (options.template) {
2611
+ const templatePath = path5.resolve(process3.cwd(), options.template);
2612
+ if (!fs4.existsSync(templatePath)) {
2613
+ throw new Error(`Template file ${options.template} does not exist.`);
2614
+ }
2615
+ if (!templatePath.endsWith(".stx")) {
2616
+ console.warn(`Warning: Template file ${options.template} does not have a .stx extension. Using it anyway.`);
2617
+ }
2618
+ templateContent = fs4.readFileSync(templatePath, "utf-8");
2619
+ console.warn(`Using template from ${options.template}`);
2620
+ } else {
2621
+ templateContent = getDefaultTemplate();
2622
+ }
2623
+ fs4.writeFileSync(filePath, templateContent);
2624
+ console.warn(`Created new stx file: ${fileName}`);
2625
+ return true;
2626
+ } catch (error) {
2627
+ console.error(`Error creating file: ${error instanceof Error ? error.message : String(error)}`);
2628
+ return false;
166
2629
  }
167
2630
  }
168
- async function cacheTemplate(filePath, output, dependencies, options) {
169
- try {
170
- const cachePath = path2.resolve(options.cachePath);
171
- await fs.promises.mkdir(cachePath, { recursive: true });
172
- const cacheFile = path2.join(cachePath, `${hashFilePath(filePath)}.html`);
173
- const metaFile = path2.join(cachePath, `${hashFilePath(filePath)}.meta.json`);
174
- const stats = await fs.promises.stat(filePath);
175
- await Bun.write(cacheFile, output);
176
- const meta = {
177
- sourcePath: filePath,
178
- mtime: stats.mtime.getTime(),
179
- dependencies: Array.from(dependencies),
180
- cacheVersion: options.cacheVersion,
181
- generatedAt: Date.now()
2631
+ function getDefaultTemplate() {
2632
+ return `<!DOCTYPE html>
2633
+ <html lang="en">
2634
+ <head>
2635
+ <meta charset="UTF-8">
2636
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2637
+ <title>{{ title }}</title>
2638
+ <script>
2639
+ export const title = "My stx Page";
2640
+ export const description = "A page built with stx";
2641
+ export const items = [
2642
+ "Templates with TypeScript support",
2643
+ "Powerful directives",
2644
+ "Reusable components"
2645
+ ];
2646
+ </script>
2647
+ <style>
2648
+ :root {
2649
+ --primary-color: #3498db;
2650
+ --dark-color: #34495e;
2651
+ --light-color: #ecf0f1;
2652
+ --font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2653
+ }
2654
+
2655
+ body {
2656
+ font-family: var(--font-main);
2657
+ line-height: 1.6;
2658
+ color: var(--dark-color);
2659
+ max-width: 800px;
2660
+ margin: 0 auto;
2661
+ padding: 2rem;
2662
+ }
2663
+
2664
+ h1 {
2665
+ color: var(--primary-color);
2666
+ }
2667
+
2668
+ .content {
2669
+ margin-top: 2rem;
2670
+ }
2671
+
2672
+ ul {
2673
+ padding-left: 1.5rem;
2674
+ }
2675
+
2676
+ li {
2677
+ margin-bottom: 0.5rem;
2678
+ }
2679
+ </style>
2680
+ </head>
2681
+ <body>
2682
+ <header>
2683
+ <h1>{{ title }}</h1>
2684
+ <p>{{ description }}</p>
2685
+ </header>
2686
+
2687
+ <div class="content">
2688
+ <h2>Features</h2>
2689
+ <ul>
2690
+ @foreach(items as item)
2691
+ <li>{{ item }}</li>
2692
+ @endforeach
2693
+ </ul>
2694
+ </div>
2695
+ </body>
2696
+ </html>`;
2697
+ }
2698
+ // src/release.ts
2699
+ var gitHash = "f9c3713";
2700
+ // src/serve.ts
2701
+ var {serve: bunServe } = globalThis.Bun;
2702
+ import fs5 from "fs";
2703
+ import path6 from "path";
2704
+ async function serve2(options = {}) {
2705
+ const {
2706
+ port = 3000,
2707
+ root = ".",
2708
+ stxOptions = {},
2709
+ watch = true,
2710
+ onRequest,
2711
+ routes = {},
2712
+ middleware = [],
2713
+ on404,
2714
+ onError
2715
+ } = options;
2716
+ const rootDir = path6.resolve(root);
2717
+ const fileCache = new Map;
2718
+ async function processStxFile(filePath) {
2719
+ const stats = fs5.statSync(filePath);
2720
+ const cacheKey = `${filePath}:${stats.mtimeMs}`;
2721
+ const cached = fileCache.get(cacheKey);
2722
+ if (cached) {
2723
+ return cached.content;
2724
+ }
2725
+ const content = await Bun.file(filePath).text();
2726
+ const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
2727
+ const scriptContent = scriptMatch ? scriptMatch[1] : "";
2728
+ const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, "");
2729
+ const context = {
2730
+ __filename: filePath,
2731
+ __dirname: path6.dirname(filePath)
182
2732
  };
183
- await Bun.write(metaFile, JSON.stringify(meta, null, 2));
184
- templateCache.set(filePath, {
185
- output,
186
- mtime: stats.mtime.getTime(),
187
- dependencies
2733
+ await extractVariables(scriptContent, context, filePath);
2734
+ const dependencies = new Set;
2735
+ const output = await processDirectives(templateContent, context, filePath, stxOptions, dependencies);
2736
+ fileCache.set(cacheKey, { content: output, mtime: stats.mtimeMs });
2737
+ return output;
2738
+ }
2739
+ async function processMarkdownFile(filePath) {
2740
+ const stats = fs5.statSync(filePath);
2741
+ const cacheKey = `${filePath}:${stats.mtimeMs}`;
2742
+ const cached = fileCache.get(cacheKey);
2743
+ if (cached) {
2744
+ return cached.content;
2745
+ }
2746
+ const { content } = await readMarkdownFile(filePath, stxOptions);
2747
+ fileCache.set(cacheKey, { content, mtime: stats.mtimeMs });
2748
+ return content;
2749
+ }
2750
+ function resolveRequestPath(pathname) {
2751
+ const relPath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
2752
+ const possiblePaths = [
2753
+ relPath,
2754
+ `${relPath}.stx`,
2755
+ `${relPath}.md`,
2756
+ `${relPath}.html`,
2757
+ path6.join(relPath, "index.stx"),
2758
+ path6.join(relPath, "index.md"),
2759
+ path6.join(relPath, "index.html")
2760
+ ];
2761
+ for (const possiblePath of possiblePaths) {
2762
+ const fullPath = path6.join(rootDir, possiblePath);
2763
+ if (fs5.existsSync(fullPath) && fs5.statSync(fullPath).isFile()) {
2764
+ return fullPath;
2765
+ }
2766
+ }
2767
+ return null;
2768
+ }
2769
+ async function handleRequest(request) {
2770
+ const url = new URL(request.url);
2771
+ let next = async () => {
2772
+ if (routes[url.pathname]) {
2773
+ return await routes[url.pathname](request);
2774
+ }
2775
+ if (onRequest) {
2776
+ const customResponse = await onRequest(request);
2777
+ if (customResponse) {
2778
+ return customResponse;
2779
+ }
2780
+ }
2781
+ const filePath = resolveRequestPath(url.pathname);
2782
+ if (!filePath) {
2783
+ if (on404) {
2784
+ return await on404(request);
2785
+ }
2786
+ return new Response("Not Found", { status: 404 });
2787
+ }
2788
+ try {
2789
+ let content;
2790
+ let contentType = "text/html";
2791
+ if (filePath.endsWith(".stx")) {
2792
+ content = await processStxFile(filePath);
2793
+ } else if (filePath.endsWith(".md")) {
2794
+ content = await processMarkdownFile(filePath);
2795
+ } else if (filePath.endsWith(".html")) {
2796
+ content = await Bun.file(filePath).text();
2797
+ } else if (filePath.endsWith(".css")) {
2798
+ content = await Bun.file(filePath).text();
2799
+ contentType = "text/css";
2800
+ } else if (filePath.endsWith(".js")) {
2801
+ content = await Bun.file(filePath).text();
2802
+ contentType = "text/javascript";
2803
+ } else if (filePath.endsWith(".json")) {
2804
+ content = await Bun.file(filePath).text();
2805
+ contentType = "application/json";
2806
+ } else {
2807
+ return new Response(Bun.file(filePath));
2808
+ }
2809
+ return new Response(content, {
2810
+ headers: { "Content-Type": contentType }
2811
+ });
2812
+ } catch (error) {
2813
+ if (onError) {
2814
+ return await onError(error, request);
2815
+ }
2816
+ return new Response(`<h1>Error</h1><pre>${error.message}
2817
+ ${error.stack}</pre>`, {
2818
+ status: 500,
2819
+ headers: { "Content-Type": "text/html" }
2820
+ });
2821
+ }
2822
+ };
2823
+ for (let i = middleware.length - 1;i >= 0; i--) {
2824
+ const mw = middleware[i];
2825
+ const currentNext = next;
2826
+ next = async () => await mw(request, currentNext);
2827
+ }
2828
+ return await next();
2829
+ }
2830
+ const server = bunServe({
2831
+ port,
2832
+ fetch: handleRequest
2833
+ });
2834
+ let watcher = null;
2835
+ if (watch) {
2836
+ watcher = fs5.watch(rootDir, { recursive: true }, (eventType, filename) => {
2837
+ if (filename && (filename.endsWith(".stx") || filename.endsWith(".md") || filename.endsWith(".html"))) {
2838
+ fileCache.clear();
2839
+ }
188
2840
  });
189
- } catch (err) {
190
- console.warn(`Failed to cache template ${filePath}:`, err);
191
2841
  }
2842
+ return {
2843
+ server,
2844
+ url: server.url.toString(),
2845
+ stop() {
2846
+ if (watcher) {
2847
+ watcher.close();
2848
+ }
2849
+ server.stop();
2850
+ }
2851
+ };
2852
+ }
2853
+ async function serveFile(filePath, options = {}) {
2854
+ const absolutePath = path6.resolve(filePath);
2855
+ if (!fs5.existsSync(absolutePath)) {
2856
+ throw new Error(`File not found: ${absolutePath}`);
2857
+ }
2858
+ const isMarkdown = absolutePath.endsWith(".md");
2859
+ const isStx = absolutePath.endsWith(".stx");
2860
+ if (!isMarkdown && !isStx) {
2861
+ throw new Error(`Unsupported file type: ${absolutePath}. Only .stx and .md files are supported.`);
2862
+ }
2863
+ return await serve2({
2864
+ ...options,
2865
+ root: path6.dirname(absolutePath),
2866
+ routes: {
2867
+ "/": async () => {
2868
+ let content;
2869
+ if (isMarkdown) {
2870
+ const { content: md } = await readMarkdownFile(absolutePath, options.stxOptions);
2871
+ content = md;
2872
+ } else {
2873
+ const fileContent = await Bun.file(absolutePath).text();
2874
+ const scriptMatch = fileContent.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
2875
+ const scriptContent = scriptMatch ? scriptMatch[1] : "";
2876
+ const templateContent = fileContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, "");
2877
+ const context = {};
2878
+ await extractVariables(scriptContent, context, absolutePath);
2879
+ const dependencies = new Set;
2880
+ content = await processDirectives(templateContent, context, absolutePath, options.stxOptions || {}, dependencies);
2881
+ }
2882
+ return new Response(content, {
2883
+ headers: { "Content-Type": "text/html" }
2884
+ });
2885
+ },
2886
+ ...options.routes
2887
+ }
2888
+ });
192
2889
  }
193
- function hashFilePath(filePath) {
194
- const hash = new Bun.CryptoHasher("sha1").update(filePath).digest("hex");
195
- return hash.substring(0, 16);
2890
+ function createMiddleware(handler) {
2891
+ return handler;
2892
+ }
2893
+ function createRoute(handler) {
2894
+ return handler;
196
2895
  }
197
2896
  // src/streaming.ts
198
- import path3 from "path";
2897
+ import path7 from "path";
199
2898
  var defaultStreamingConfig = {
200
2899
  enabled: true,
201
2900
  bufferSize: 1024 * 16,
202
2901
  strategy: "auto",
203
2902
  timeout: 30000
204
2903
  };
205
- var SECTION_PATTERN = /<!-- @section:([a-zA-Z0-9_-]+) -->([\s\S]*?)<!-- @endsection:\1 -->/g;
2904
+ var SECTION_PATTERN = /<!-- @section:([\w-]+) -->([\s\S]*?)<!-- @endsection:\1 -->/g;
206
2905
  async function streamTemplate(templatePath, data = {}, options = {}) {
207
2906
  const fullOptions = {
208
2907
  ...defaultConfig,
@@ -222,7 +2921,7 @@ async function streamTemplate(templatePath, data = {}, options = {}) {
222
2921
  const context = {
223
2922
  ...data,
224
2923
  __filename: templatePath,
225
- __dirname: path3.dirname(templatePath)
2924
+ __dirname: path7.dirname(templatePath)
226
2925
  };
227
2926
  await extractVariables(scriptContent, context, templatePath);
228
2927
  const dependencies = new Set;
@@ -244,7 +2943,7 @@ async function createStreamRenderer(templatePath, options = {}) {
244
2943
  ...options.streaming
245
2944
  }
246
2945
  };
247
- let content = await Bun.file(templatePath).text();
2946
+ const content = await Bun.file(templatePath).text();
248
2947
  const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
249
2948
  const scriptContent = scriptMatch ? scriptMatch[1] : "";
250
2949
  const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, "");
@@ -257,13 +2956,13 @@ async function createStreamRenderer(templatePath, options = {}) {
257
2956
  const sectionContent = match[2];
258
2957
  sections[sectionName] = sectionContent;
259
2958
  }
260
- let shellTemplate = templateContent.replace(SECTION_PATTERN, "");
2959
+ const shellTemplate = templateContent.replace(SECTION_PATTERN, "");
261
2960
  const renderer = {
262
2961
  renderShell: async (data = {}) => {
263
2962
  const context = {
264
2963
  ...data,
265
2964
  __filename: templatePath,
266
- __dirname: path3.dirname(templatePath)
2965
+ __dirname: path7.dirname(templatePath)
267
2966
  };
268
2967
  await extractVariables(scriptContent, context, templatePath);
269
2968
  const dependencies = new Set;
@@ -277,7 +2976,7 @@ async function createStreamRenderer(templatePath, options = {}) {
277
2976
  const context = {
278
2977
  ...data,
279
2978
  __filename: templatePath,
280
- __dirname: path3.dirname(templatePath)
2979
+ __dirname: path7.dirname(templatePath)
281
2980
  };
282
2981
  await extractVariables(scriptContent, context, templatePath);
283
2982
  const dependencies = new Set;
@@ -299,7 +2998,7 @@ async function createStreamRenderer(templatePath, options = {}) {
299
2998
  var islandDirective = {
300
2999
  name: "island",
301
3000
  hasEndTag: true,
302
- handler: (content, params, context, filePath) => {
3001
+ handler: (content, params, _context, _filePath) => {
303
3002
  if (!params || params.length === 0) {
304
3003
  throw new Error("Island directive requires a name parameter");
305
3004
  }
@@ -324,29 +3023,50 @@ function registerStreamingDirectives(options = {}) {
324
3023
  }
325
3024
  return directives;
326
3025
  }
327
- async function processSectionDirectives(content, context, filePath, options = {}) {
3026
+ async function processSectionDirectives(content, context, filePath, _options = {}) {
328
3027
  return content;
329
3028
  }
330
3029
 
331
3030
  // src/index.ts
332
- var src_default = plugin;
3031
+ var src_default = {};
333
3032
  export {
334
3033
  webComponentDirectiveHandler,
3034
+ validators,
335
3035
  unescapeHtml,
3036
+ transitionDirective,
336
3037
  templateCache,
3038
+ structuredDataDirective,
337
3039
  streamTemplate,
338
3040
  setGlobalContext,
3041
+ serveStxFile,
3042
+ serveMultipleStxFiles,
3043
+ serveFile,
3044
+ serve2 as serve,
3045
+ scrollAnimateDirective,
3046
+ screenReaderDirective,
3047
+ scanA11yIssues,
3048
+ safeExecuteAsync,
3049
+ safeExecute,
339
3050
  runPreProcessingMiddleware,
340
3051
  runPostProcessingMiddleware,
341
3052
  resolveTemplatePath,
342
3053
  renderComponent,
343
3054
  registerStreamingDirectives,
3055
+ registerSeoDirectives,
3056
+ registerComponentDirectives,
3057
+ registerAnimationDirectives,
3058
+ registerA11yDirectives,
3059
+ readMarkdownFile,
344
3060
  processTranslateDirective,
3061
+ processStructuredData,
345
3062
  processStackReplacements,
346
3063
  processStackPushDirectives,
3064
+ processSeoDirective,
347
3065
  processSectionDirectives,
348
3066
  processOnceDirective,
349
3067
  processMiddleware,
3068
+ processMetaDirectives,
3069
+ processMarkdownFileDirectives,
350
3070
  processMarkdownDirectives,
351
3071
  processLoops,
352
3072
  processJsonDirective,
@@ -359,18 +3079,29 @@ export {
359
3079
  processDirectives,
360
3080
  processCustomDirectives,
361
3081
  processBasicFormDirectives,
3082
+ processAnimationDirectives,
3083
+ processA11yDirectives,
362
3084
  partialsCache,
3085
+ onceStore,
3086
+ motionDirective,
3087
+ metaDirective,
363
3088
  markdownDirectiveHandler,
3089
+ markdownCache,
364
3090
  loadTranslation,
365
3091
  islandDirective,
3092
+ injectSeoTags,
3093
+ initFile,
366
3094
  hashFilePath,
3095
+ gitHash,
367
3096
  getTranslation,
368
3097
  getSourceLineInfo,
3098
+ getScreenReaderOnlyStyle,
369
3099
  generateTemplatesDocs,
370
3100
  generateDocs,
371
3101
  generateDirectivesDocs,
372
3102
  generateComponentsDocs,
373
3103
  generateComponentDoc,
3104
+ formatStxContent,
374
3105
  formatDocsAsMarkdown,
375
3106
  formatDocsAsJson,
376
3107
  formatDocsAsHtml,
@@ -382,16 +3113,43 @@ export {
382
3113
  evaluateExpression,
383
3114
  evaluateAuthExpression,
384
3115
  escapeHtml,
3116
+ errorRecovery,
3117
+ errorLogger,
385
3118
  docsCommand,
3119
+ devHelpers,
3120
+ defineStxConfig,
3121
+ defaultI18nConfig,
386
3122
  defaultFilters,
387
3123
  defaultConfig,
388
3124
  src_default as default,
389
3125
  createTranslateFilter,
390
3126
  createStreamRenderer,
3127
+ createRoute,
3128
+ createMiddleware,
3129
+ createEnhancedError,
391
3130
  createDetailedErrorMessage,
392
3131
  config,
3132
+ componentDirective,
3133
+ clearOnceStore,
393
3134
  checkCache,
3135
+ checkA11y,
394
3136
  cacheTemplate,
395
3137
  buildWebComponents,
396
- applyFilters
3138
+ applyFilters,
3139
+ animationGroupDirective,
3140
+ analyzeTemplate,
3141
+ analyzeProject,
3142
+ a11yDirective,
3143
+ TransitionType,
3144
+ TransitionEase,
3145
+ TransitionDirection,
3146
+ StxSyntaxError,
3147
+ StxSecurityError,
3148
+ StxRuntimeError,
3149
+ StxFileError,
3150
+ StxError,
3151
+ ErrorLogger,
3152
+ DEFAULT_TRANSITION_OPTIONS
397
3153
  };
3154
+
3155
+ export { analyzeProject, plugin, serveStxFile, serveMultipleStxFiles, docsCommand, formatStxContent, initFile, gitHash };