@buenojs/bueno 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads templates from the filesystem with support for:
|
|
5
|
+
* - YAML front matter (metadata)
|
|
6
|
+
* - Template variants (channel-specific sections)
|
|
7
|
+
* - In-memory caching with TTL
|
|
8
|
+
* - Hot reload in development mode
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, watch } from "fs";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { extname, resolve } from "path";
|
|
14
|
+
import type {
|
|
15
|
+
Template,
|
|
16
|
+
TemplateLoaderOptions,
|
|
17
|
+
TemplateMetadata,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses YAML-like front matter from template
|
|
22
|
+
* Format: ---\nkey: value\n---\n
|
|
23
|
+
*/
|
|
24
|
+
function parseFrontMatter(content: string): {
|
|
25
|
+
metadata: TemplateMetadata;
|
|
26
|
+
body: string;
|
|
27
|
+
} {
|
|
28
|
+
const lines = content.split("\n");
|
|
29
|
+
|
|
30
|
+
// Check for front matter delimiter
|
|
31
|
+
if (lines[0]?.trim() !== "---") {
|
|
32
|
+
return { metadata: {}, body: content };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Find closing delimiter
|
|
36
|
+
let endIdx = -1;
|
|
37
|
+
for (let i = 1; i < lines.length; i++) {
|
|
38
|
+
if (lines[i]?.trim() === "---") {
|
|
39
|
+
endIdx = i;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (endIdx === -1) {
|
|
45
|
+
return { metadata: {}, body: content };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse front matter
|
|
49
|
+
const frontMatterLines = lines.slice(1, endIdx);
|
|
50
|
+
const metadata: TemplateMetadata = {};
|
|
51
|
+
|
|
52
|
+
for (const line of frontMatterLines) {
|
|
53
|
+
if (!line.trim()) continue;
|
|
54
|
+
|
|
55
|
+
const colonIdx = line.indexOf(":");
|
|
56
|
+
if (colonIdx === -1) continue;
|
|
57
|
+
|
|
58
|
+
const key = line.substring(0, colonIdx).trim();
|
|
59
|
+
let value: unknown = line.substring(colonIdx + 1).trim();
|
|
60
|
+
|
|
61
|
+
// Simple type conversion
|
|
62
|
+
if (value === "true") value = true;
|
|
63
|
+
else if (value === "false") value = false;
|
|
64
|
+
else if (value === "null") value = null;
|
|
65
|
+
else if (!isNaN(Number(value)) && value !== "") value = Number(value);
|
|
66
|
+
else if (value.startsWith("[") && value.endsWith("]")) {
|
|
67
|
+
// Parse simple arrays: [email, sms, push] or ["email", "sms", "push"]
|
|
68
|
+
const arrayContent = value.slice(1, -1).trim();
|
|
69
|
+
value = arrayContent.split(",").map((v) => {
|
|
70
|
+
let item = v.trim();
|
|
71
|
+
// Remove quotes if present
|
|
72
|
+
if (
|
|
73
|
+
(item.startsWith('"') && item.endsWith('"')) ||
|
|
74
|
+
(item.startsWith("'") && item.endsWith("'"))
|
|
75
|
+
) {
|
|
76
|
+
item = item.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
return item;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
metadata[key] = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const body = lines.slice(endIdx + 1).join("\n");
|
|
86
|
+
return { metadata, body };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses template variants from sections
|
|
91
|
+
* Format: ## Email\n...\n---\n## SMS\n...\n---\n## Push\n...
|
|
92
|
+
*
|
|
93
|
+
* Supports:
|
|
94
|
+
* - Single variant per section: "## Email"
|
|
95
|
+
* - Multiple variants (slash): "## SMS/Push"
|
|
96
|
+
* - Multiple variants (comma): "## SMS, Push"
|
|
97
|
+
*/
|
|
98
|
+
function parseVariants(content: string): Record<string, string> {
|
|
99
|
+
const sections = content.split(/^## /m);
|
|
100
|
+
const variants: Record<string, string> = {};
|
|
101
|
+
|
|
102
|
+
for (const section of sections) {
|
|
103
|
+
if (!section.trim()) continue;
|
|
104
|
+
|
|
105
|
+
const lines = section.split("\n");
|
|
106
|
+
const headerLine = lines[0];
|
|
107
|
+
|
|
108
|
+
if (!headerLine) continue;
|
|
109
|
+
|
|
110
|
+
// Extract variant names from header
|
|
111
|
+
// Supports: "Email", "SMS/Push", "SMS, Push", etc.
|
|
112
|
+
const variantNames = headerLine
|
|
113
|
+
.split(/[,/]\s*/)
|
|
114
|
+
.map((s) => s.trim().toLowerCase())
|
|
115
|
+
.filter((s) => s);
|
|
116
|
+
|
|
117
|
+
// Get section content (skip header and empty lines at start)
|
|
118
|
+
const contentLines = lines.slice(1);
|
|
119
|
+
let contentStart = 0;
|
|
120
|
+
while (
|
|
121
|
+
contentStart < contentLines.length &&
|
|
122
|
+
!contentLines[contentStart]?.trim()
|
|
123
|
+
) {
|
|
124
|
+
contentStart++;
|
|
125
|
+
}
|
|
126
|
+
const sectionContent = contentLines.slice(contentStart).join("\n").trim();
|
|
127
|
+
|
|
128
|
+
// Handle separator line (---)
|
|
129
|
+
const separatorIdx = sectionContent.indexOf("\n---");
|
|
130
|
+
const finalContent =
|
|
131
|
+
separatorIdx !== -1
|
|
132
|
+
? sectionContent.substring(0, separatorIdx)
|
|
133
|
+
: sectionContent;
|
|
134
|
+
|
|
135
|
+
// Map all variant names to same content
|
|
136
|
+
for (const variantName of variantNames) {
|
|
137
|
+
variants[variantName] = finalContent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return variants;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detects template format from file extension or metadata
|
|
146
|
+
*/
|
|
147
|
+
function detectFormat(
|
|
148
|
+
filePath: string,
|
|
149
|
+
metadata: TemplateMetadata,
|
|
150
|
+
): "markdown" | "text" | "html" {
|
|
151
|
+
// Check metadata override
|
|
152
|
+
if (metadata.format) {
|
|
153
|
+
const fmt = String(metadata.format).toLowerCase();
|
|
154
|
+
if (["markdown", "text", "html"].includes(fmt)) {
|
|
155
|
+
return fmt as "markdown" | "text" | "html";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Detect from extension
|
|
160
|
+
const ext = extname(filePath).toLowerCase();
|
|
161
|
+
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
162
|
+
if (ext === ".txt" || ext === ".text") return "text";
|
|
163
|
+
if (ext === ".html" || ext === ".htm") return "html";
|
|
164
|
+
|
|
165
|
+
// Default to markdown (most common)
|
|
166
|
+
return "markdown";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Template loader with caching and hot reload
|
|
171
|
+
*/
|
|
172
|
+
export class TemplateLoader {
|
|
173
|
+
private cache: Map<string, { template: Template; timestamp: number }> =
|
|
174
|
+
new Map();
|
|
175
|
+
private watchers: Map<string, ReturnType<typeof watch>> = new Map();
|
|
176
|
+
private metrics = {
|
|
177
|
+
loads: 0,
|
|
178
|
+
cacheHits: 0,
|
|
179
|
+
cacheMisses: 0,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
constructor(private options: TemplateLoaderOptions) {
|
|
183
|
+
// Set defaults
|
|
184
|
+
this.options.cacheEnabled = options.cacheEnabled ?? true;
|
|
185
|
+
this.options.cacheTtl = options.cacheTtl ?? 3600;
|
|
186
|
+
this.options.maxCacheSize = options.maxCacheSize ?? 100;
|
|
187
|
+
this.options.extension = options.extension ?? ".md";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Load a template by ID (path like "emails/welcome")
|
|
192
|
+
*/
|
|
193
|
+
load(templateId: string): Template {
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
|
|
196
|
+
// Check cache
|
|
197
|
+
if (this.options.cacheEnabled) {
|
|
198
|
+
const cached = this.cache.get(templateId);
|
|
199
|
+
if (cached) {
|
|
200
|
+
const age = (now - cached.timestamp) / 1000;
|
|
201
|
+
if (age < (this.options.cacheTtl ?? 3600)) {
|
|
202
|
+
this.metrics.cacheHits++;
|
|
203
|
+
return cached.template;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Load from disk
|
|
209
|
+
this.metrics.cacheMisses++;
|
|
210
|
+
const template = this._loadFromDisk(templateId);
|
|
211
|
+
|
|
212
|
+
// Cache it
|
|
213
|
+
if (this.options.cacheEnabled) {
|
|
214
|
+
// Prune cache if too large
|
|
215
|
+
if (
|
|
216
|
+
this.cache.size >= (this.options.maxCacheSize ?? 100) &&
|
|
217
|
+
!this.cache.has(templateId)
|
|
218
|
+
) {
|
|
219
|
+
// Remove oldest entry
|
|
220
|
+
const oldest = Array.from(this.cache.entries()).sort(
|
|
221
|
+
(a, b) => a[1].timestamp - b[1].timestamp,
|
|
222
|
+
)[0];
|
|
223
|
+
if (oldest) {
|
|
224
|
+
this.cache.delete(oldest[0]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.cache.set(templateId, { template, timestamp: now });
|
|
229
|
+
|
|
230
|
+
// Set up file watcher in development mode
|
|
231
|
+
if (this.options.watch) {
|
|
232
|
+
this._watchTemplate(templateId, template.id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.metrics.loads++;
|
|
237
|
+
return template;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Load template from disk with variant parsing
|
|
242
|
+
*/
|
|
243
|
+
private _loadFromDisk(templateId: string): Template {
|
|
244
|
+
// Try multiple extensions
|
|
245
|
+
let filePath = resolve(
|
|
246
|
+
this.options.basePath,
|
|
247
|
+
templateId + this.options.extension,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!existsSync(filePath)) {
|
|
251
|
+
// Try .md if original extension didn't work
|
|
252
|
+
filePath = resolve(this.options.basePath, templateId + ".md");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!existsSync(filePath)) {
|
|
256
|
+
// Try .txt
|
|
257
|
+
filePath = resolve(this.options.basePath, templateId + ".txt");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!existsSync(filePath)) {
|
|
261
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Read file
|
|
265
|
+
const content = readFileSync(filePath, "utf-8");
|
|
266
|
+
|
|
267
|
+
// Parse front matter
|
|
268
|
+
const { metadata, body } = parseFrontMatter(content);
|
|
269
|
+
|
|
270
|
+
// Parse variants from body
|
|
271
|
+
const variants = parseVariants(body);
|
|
272
|
+
|
|
273
|
+
// If no variants found, treat entire body as default variant
|
|
274
|
+
if (Object.keys(variants).length === 0) {
|
|
275
|
+
const defaultVariant = metadata.default || "default";
|
|
276
|
+
variants[defaultVariant] = body;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Detect format
|
|
280
|
+
const format = detectFormat(filePath, metadata);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
id: templateId,
|
|
284
|
+
format,
|
|
285
|
+
content: body,
|
|
286
|
+
variants,
|
|
287
|
+
metadata,
|
|
288
|
+
loadedAt: Date.now(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Watch template file for changes (development mode)
|
|
294
|
+
*/
|
|
295
|
+
private _watchTemplate(templateId: string, filePath: string): void {
|
|
296
|
+
if (this.watchers.has(templateId)) {
|
|
297
|
+
return; // Already watching
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const fullPath = resolve(this.options.basePath, filePath);
|
|
302
|
+
const watcher = watch(fullPath, () => {
|
|
303
|
+
// Invalidate cache on file change
|
|
304
|
+
this.cache.delete(templateId);
|
|
305
|
+
this.watchers.delete(templateId);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
this.watchers.set(templateId, watcher);
|
|
309
|
+
} catch {
|
|
310
|
+
// Silently fail if file watching not available
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Clear all caches and watchers
|
|
316
|
+
*/
|
|
317
|
+
clear(): void {
|
|
318
|
+
this.cache.clear();
|
|
319
|
+
for (const watcher of this.watchers.values()) {
|
|
320
|
+
watcher.close();
|
|
321
|
+
}
|
|
322
|
+
this.watchers.clear();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get loader metrics
|
|
327
|
+
*/
|
|
328
|
+
getMetrics() {
|
|
329
|
+
return { ...this.metrics };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer
|
|
3
|
+
*
|
|
4
|
+
* Converts Markdown to HTML or plain text.
|
|
5
|
+
* Lightweight implementation supporting:
|
|
6
|
+
* - Headings (# ## ###)
|
|
7
|
+
* - Bold/italic (*text*, **text**)
|
|
8
|
+
* - Lists (- item, 1. item)
|
|
9
|
+
* - Links [text](url)
|
|
10
|
+
* - Code (`inline`)
|
|
11
|
+
* - Blockquotes (> quote)
|
|
12
|
+
* - Paragraphs (blank line separated)
|
|
13
|
+
*
|
|
14
|
+
* NOT supported (kept lightweight):
|
|
15
|
+
* - Tables
|
|
16
|
+
* - Code blocks with syntax highlighting
|
|
17
|
+
* - Complex nesting
|
|
18
|
+
* - Strikethrough
|
|
19
|
+
* - Footnotes
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Markdown to HTML renderer
|
|
24
|
+
*/
|
|
25
|
+
export class MarkdownRenderer {
|
|
26
|
+
/**
|
|
27
|
+
* Convert Markdown string to HTML
|
|
28
|
+
*/
|
|
29
|
+
static toHtml(markdown: string): string {
|
|
30
|
+
let html = markdown;
|
|
31
|
+
|
|
32
|
+
// Escape HTML special characters (but preserve user-intentional HTML)
|
|
33
|
+
// Only escape in content sections (not in already-formed tags)
|
|
34
|
+
|
|
35
|
+
// Process in order of precedence (inner → outer)
|
|
36
|
+
|
|
37
|
+
// 1. Inline code: `code`
|
|
38
|
+
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
39
|
+
|
|
40
|
+
// 2. Bold: **text** or __text__
|
|
41
|
+
html = html.replace(/\*\*([^\*]+)\*\*/g, "<strong>$1</strong>");
|
|
42
|
+
html = html.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
43
|
+
|
|
44
|
+
// 3. Italic: *text* or _text_
|
|
45
|
+
// (be careful not to match ** or __)
|
|
46
|
+
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, "<em>$1</em>");
|
|
47
|
+
html = html.replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>");
|
|
48
|
+
|
|
49
|
+
// 4. Links: [text](url)
|
|
50
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
51
|
+
|
|
52
|
+
// 5. Line breaks (double space at end of line or \n\n)
|
|
53
|
+
html = html.replace(/ {2}\n/g, "<br>\n");
|
|
54
|
+
|
|
55
|
+
// 6. Blockquotes: > quote
|
|
56
|
+
html = html.replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>");
|
|
57
|
+
|
|
58
|
+
// 7. Headings (must be on own line)
|
|
59
|
+
html = html.replace(/^### ([^\n]+)$/gm, "<h3>$1</h3>");
|
|
60
|
+
html = html.replace(/^## ([^\n]+)$/gm, "<h2>$1</h2>");
|
|
61
|
+
html = html.replace(/^# ([^\n]+)$/gm, "<h1>$1</h1>");
|
|
62
|
+
|
|
63
|
+
// 8. Ordered lists: 1. 2. 3.
|
|
64
|
+
html = this._processOrderedLists(html);
|
|
65
|
+
|
|
66
|
+
// 9. Unordered lists: - * +
|
|
67
|
+
html = this._processUnorderedLists(html);
|
|
68
|
+
|
|
69
|
+
// 10. Paragraphs (blank line = new paragraph)
|
|
70
|
+
html = this._processParagraphs(html);
|
|
71
|
+
|
|
72
|
+
// 11. Horizontal rules: ---
|
|
73
|
+
html = html.replace(/^---$/gm, "<hr>");
|
|
74
|
+
|
|
75
|
+
return html.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert Markdown string to plain text
|
|
80
|
+
*/
|
|
81
|
+
static toText(markdown: string): string {
|
|
82
|
+
let text = markdown;
|
|
83
|
+
|
|
84
|
+
// Remove inline code markers: `code` → code
|
|
85
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
86
|
+
|
|
87
|
+
// Remove bold markers: **text** → text
|
|
88
|
+
text = text.replace(/\*\*([^\*]+)\*\*/g, "$1");
|
|
89
|
+
text = text.replace(/__([^_]+)__/g, "$1");
|
|
90
|
+
|
|
91
|
+
// Remove italic markers: *text* → text
|
|
92
|
+
text = text.replace(/\*([^\*]+)\*/g, "$1");
|
|
93
|
+
text = text.replace(/_([^_]+)_/g, "$1");
|
|
94
|
+
|
|
95
|
+
// Convert links: [text](url) → text: url
|
|
96
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1: $2");
|
|
97
|
+
|
|
98
|
+
// Remove blockquote markers: > quote → quote
|
|
99
|
+
text = text.replace(/^> (.+)$/gm, "$1");
|
|
100
|
+
|
|
101
|
+
// Remove headings markers: # text → text
|
|
102
|
+
text = text.replace(/^#+\s+(.+)$/gm, "$1");
|
|
103
|
+
|
|
104
|
+
// Remove list markers
|
|
105
|
+
text = text.replace(/^\d+\.\s+/gm, ""); // Ordered: 1. 2. 3.
|
|
106
|
+
text = text.replace(/^[-*+]\s+/gm, ""); // Unordered: - * +
|
|
107
|
+
|
|
108
|
+
// Clean up multiple blank lines
|
|
109
|
+
text = text.replace(/\n\n\n+/g, "\n\n");
|
|
110
|
+
|
|
111
|
+
return text.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Process ordered lists (1. 2. 3.)
|
|
116
|
+
*/
|
|
117
|
+
private static _processOrderedLists(html: string): string {
|
|
118
|
+
// Find list blocks
|
|
119
|
+
const lines = html.split("\n");
|
|
120
|
+
let inList = false;
|
|
121
|
+
let listHtml = "";
|
|
122
|
+
const result: string[] = [];
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
const match = line.match(/^(\d+)\.\s+(.+)$/);
|
|
127
|
+
|
|
128
|
+
if (match) {
|
|
129
|
+
if (!inList) {
|
|
130
|
+
inList = true;
|
|
131
|
+
listHtml = `<ol>\n<li>${match[2]}</li>`;
|
|
132
|
+
} else {
|
|
133
|
+
listHtml += `\n<li>${match[2]}</li>`;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if (inList) {
|
|
137
|
+
result.push(listHtml + "\n</ol>");
|
|
138
|
+
inList = false;
|
|
139
|
+
listHtml = "";
|
|
140
|
+
}
|
|
141
|
+
result.push(line);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (inList) {
|
|
146
|
+
result.push(listHtml + "\n</ol>");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result.join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Process unordered lists (- * +)
|
|
154
|
+
*/
|
|
155
|
+
private static _processUnorderedLists(html: string): string {
|
|
156
|
+
// Find list blocks
|
|
157
|
+
const lines = html.split("\n");
|
|
158
|
+
let inList = false;
|
|
159
|
+
let listHtml = "";
|
|
160
|
+
const result: string[] = [];
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < lines.length; i++) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
const match = line.match(/^[-*+]\s+(.+)$/);
|
|
165
|
+
|
|
166
|
+
if (match) {
|
|
167
|
+
if (!inList) {
|
|
168
|
+
inList = true;
|
|
169
|
+
listHtml = `<ul>\n<li>${match[1]}</li>`;
|
|
170
|
+
} else {
|
|
171
|
+
listHtml += `\n<li>${match[1]}</li>`;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (inList) {
|
|
175
|
+
result.push(listHtml + "\n</ul>");
|
|
176
|
+
inList = false;
|
|
177
|
+
listHtml = "";
|
|
178
|
+
}
|
|
179
|
+
result.push(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (inList) {
|
|
184
|
+
result.push(listHtml + "\n</ul>");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Process paragraphs (blank line separated)
|
|
192
|
+
*/
|
|
193
|
+
private static _processParagraphs(html: string): string {
|
|
194
|
+
const blocks = html.split(/\n\n+/);
|
|
195
|
+
const result: string[] = [];
|
|
196
|
+
|
|
197
|
+
for (const block of blocks) {
|
|
198
|
+
if (!block.trim()) continue;
|
|
199
|
+
|
|
200
|
+
// Skip if already wrapped in tag
|
|
201
|
+
if (block.match(/^<[a-z]/) || block.match(/^<\/[a-z]/)) {
|
|
202
|
+
result.push(block);
|
|
203
|
+
} else if (block.match(/^<(h[1-6]|ul|ol|blockquote|hr|code)/)) {
|
|
204
|
+
result.push(block);
|
|
205
|
+
} else {
|
|
206
|
+
result.push(`<p>${block}</p>`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result.join("\n");
|
|
211
|
+
}
|
|
212
|
+
}
|