UNPKG

@stacksjs/stx

Version:

A Bun plugin that allows for using Laravel Blade-like syntax.

398 lines (395 loc) 12.7 kB
// @bun import { buildWebComponents, config, defaultConfig, docsCommand, extractComponentDescription, extractComponentProps, findComponentFiles, formatDocsAsHtml, formatDocsAsJson, formatDocsAsMarkdown, generateComponentDoc, generateComponentsDocs, generateDirectivesDocs, generateDocs, generateTemplatesDocs, webComponentDirectiveHandler } from "../chunk-9ynf73q9.js"; import { applyFilters, createDetailedErrorMessage, createTranslateFilter, defaultFilters, escapeHtml, evaluateAuthExpression, evaluateExpression, extractVariables, fileExists, getSourceLineInfo, getTranslation, loadTranslation, markdownDirectiveHandler, partialsCache, processBasicFormDirectives, processCustomDirectives, processDirectives, processErrorDirective, processExpressions, processFormDirectives, processFormInputDirectives, processForms, processIncludes, processJsonDirective, processLoops, processMarkdownDirectives, processMiddleware, processOnceDirective, processStackPushDirectives, processStackReplacements, processTranslateDirective, renderComponent, resolveTemplatePath, runPostProcessingMiddleware, runPreProcessingMiddleware, setGlobalContext, unescapeHtml } from "../chunk-8ehp5m3y.js"; import"../chunk-ywm063e4.js"; // ../bun-plugin/src/index.ts import path from "path"; var plugin = { name: "bun-plugin-stx", async setup(build) { const options = { ...defaultConfig, ...build.config?.stx }; const allDependencies = new Set; const webComponentsPath = options.webComponents?.enabled ? `./${path.relative(path.dirname(build.config?.outdir || "dist"), options.webComponents.outputDir || "dist/web-components")}` : "/web-components"; const builtComponents = []; if (options.webComponents?.enabled) { try { const components = await buildWebComponents(options, allDependencies); builtComponents.push(...components); if (options.debug && components.length > 0) { console.log(`Successfully built ${components.length} web components`); } } catch (error) { console.error("Failed to build web components:", error); } } build.onLoad({ filter: /\.stx$/ }, async ({ path: filePath }) => { try { const dependencies = new Set; if (options.cache && options.cachePath) { const cachedOutput = await checkCache(filePath, options); if (cachedOutput) { if (options.debug) { console.log(`Using cached version of ${filePath}`); } return { contents: cachedOutput, loader: "html" }; } } const content = await Bun.file(filePath).text(); const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i); const scriptContent = scriptMatch ? scriptMatch[1] : ""; const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, ""); const context = { __filename: filePath, __dirname: path.dirname(filePath), __stx: { webComponentsPath, builtComponents } }; await extractVariables(scriptContent, context, filePath); let output = templateContent; output = await processDirectives(output, context, filePath, options, dependencies); dependencies.forEach((dep) => allDependencies.add(dep)); if (options.cache && options.cachePath) { await cacheTemplate(filePath, output, dependencies, options); if (options.debug) { console.log(`Cached template ${filePath} with ${dependencies.size} dependencies`); } } return { contents: output, loader: "html" }; } catch (error) { console.error("STX Plugin Error:", error); return { contents: `<!DOCTYPE html><html><body><h1>STX Rendering Error</h1><pre>${error.message || String(error)}</pre></body></html>`, loader: "html" }; } }); } }; // src/caching.ts import fs from "fs"; import path2 from "path"; var templateCache = new Map; async function checkCache(filePath, options) { try { const cachePath = path2.resolve(options.cachePath); const cacheFile = path2.join(cachePath, `${hashFilePath(filePath)}.html`); const metaFile = path2.join(cachePath, `${hashFilePath(filePath)}.meta.json`); if (!await fileExists(cacheFile) || !await fileExists(metaFile)) return null; const metaContent = await Bun.file(metaFile).text(); const meta = JSON.parse(metaContent); if (meta.cacheVersion !== options.cacheVersion) return null; const stats = await fs.promises.stat(filePath); if (stats.mtime.getTime() > meta.mtime) return null; for (const dep of meta.dependencies) { if (await fileExists(dep)) { const depStats = await fs.promises.stat(dep); if (depStats.mtime.getTime() > meta.mtime) return null; } else { return null; } } return await Bun.file(cacheFile).text(); } catch (err) { console.warn(`Cache error for ${filePath}:`, err); return null; } } async function cacheTemplate(filePath, output, dependencies, options) { try { const cachePath = path2.resolve(options.cachePath); await fs.promises.mkdir(cachePath, { recursive: true }); const cacheFile = path2.join(cachePath, `${hashFilePath(filePath)}.html`); const metaFile = path2.join(cachePath, `${hashFilePath(filePath)}.meta.json`); const stats = await fs.promises.stat(filePath); await Bun.write(cacheFile, output); const meta = { sourcePath: filePath, mtime: stats.mtime.getTime(), dependencies: Array.from(dependencies), cacheVersion: options.cacheVersion, generatedAt: Date.now() }; await Bun.write(metaFile, JSON.stringify(meta, null, 2)); templateCache.set(filePath, { output, mtime: stats.mtime.getTime(), dependencies }); } catch (err) { console.warn(`Failed to cache template ${filePath}:`, err); } } function hashFilePath(filePath) { const hash = new Bun.CryptoHasher("sha1").update(filePath).digest("hex"); return hash.substring(0, 16); } // src/streaming.ts import path3 from "path"; var defaultStreamingConfig = { enabled: true, bufferSize: 1024 * 16, strategy: "auto", timeout: 30000 }; var SECTION_PATTERN = /<!-- @section:([a-zA-Z0-9_-]+) -->([\s\S]*?)<!-- @endsection:\1 -->/g; async function streamTemplate(templatePath, data = {}, options = {}) { const fullOptions = { ...defaultConfig, ...options, streaming: { ...defaultStreamingConfig, ...options.streaming } }; return new ReadableStream({ async start(controller) { try { const content = await Bun.file(templatePath).text(); const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i); const scriptContent = scriptMatch ? scriptMatch[1] : ""; const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, ""); const context = { ...data, __filename: templatePath, __dirname: path3.dirname(templatePath) }; await extractVariables(scriptContent, context, templatePath); const dependencies = new Set; const output = await processDirectives(templateContent, context, templatePath, fullOptions, dependencies); controller.enqueue(output); controller.close(); } catch (error) { controller.error(error); } } }); } async function createStreamRenderer(templatePath, options = {}) { const fullOptions = { ...defaultConfig, ...options, streaming: { ...defaultStreamingConfig, ...options.streaming } }; let content = await Bun.file(templatePath).text(); const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i); const scriptContent = scriptMatch ? scriptMatch[1] : ""; const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/i, ""); const originalTemplate = templateContent; const sections = {}; let match; SECTION_PATTERN.lastIndex = 0; while ((match = SECTION_PATTERN.exec(templateContent)) !== null) { const sectionName = match[1]; const sectionContent = match[2]; sections[sectionName] = sectionContent; } let shellTemplate = templateContent.replace(SECTION_PATTERN, ""); const renderer = { renderShell: async (data = {}) => { const context = { ...data, __filename: templatePath, __dirname: path3.dirname(templatePath) }; await extractVariables(scriptContent, context, templatePath); const dependencies = new Set; return processDirectives(shellTemplate, context, templatePath, fullOptions, dependencies); }, renderSection: async (sectionName, data = {}) => { try { if (!sections[sectionName]) { return `<div class="error-message">Section "${sectionName}" not found in template "${templatePath}"</div>`; } const context = { ...data, __filename: templatePath, __dirname: path3.dirname(templatePath) }; await extractVariables(scriptContent, context, templatePath); const dependencies = new Set; return await processDirectives(sections[sectionName], context, templatePath, fullOptions, dependencies); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return createDetailedErrorMessage("Expression", errorMessage, templatePath, sections[sectionName] || "", 0, sections[sectionName] || ""); } }, getSections: () => { return Object.keys(sections); }, getTemplate: () => { return originalTemplate; } }; return renderer; } var islandDirective = { name: "island", hasEndTag: true, handler: (content, params, context, filePath) => { if (!params || params.length === 0) { throw new Error("Island directive requires a name parameter"); } const islandName = params[0].replace(/['"`]/g, ""); const priority = params[1] ? params[1].replace(/['"`]/g, "") : "lazy"; const id = `island-${islandName}-${Math.random().toString(36).substring(2, 9)}`; const propsMatch = content.match(/<script\s+props\s*>([\s\S]*?)<\/script>/i); const propsScript = propsMatch ? propsMatch[1].trim() : ""; const contentWithoutProps = propsMatch ? content.replace(propsMatch[0], "") : content; return `<div data-island="${islandName}" data-island-id="${id}" data-priority="${priority}"> ${contentWithoutProps} <script type="application/json" data-island-props="${id}"> ${propsScript ? `{${propsScript}}` : "{}"} </script> </div>`; } }; function registerStreamingDirectives(options = {}) { const directives = []; if (options.hydration?.enabled) { directives.push(islandDirective); } return directives; } async function processSectionDirectives(content, context, filePath, options = {}) { return content; } // src/index.ts var src_default = plugin; export { webComponentDirectiveHandler, unescapeHtml, templateCache, streamTemplate, setGlobalContext, runPreProcessingMiddleware, runPostProcessingMiddleware, resolveTemplatePath, renderComponent, registerStreamingDirectives, processTranslateDirective, processStackReplacements, processStackPushDirectives, processSectionDirectives, processOnceDirective, processMiddleware, processMarkdownDirectives, processLoops, processJsonDirective, processIncludes, processForms, processFormInputDirectives, processFormDirectives, processExpressions, processErrorDirective, processDirectives, processCustomDirectives, processBasicFormDirectives, partialsCache, markdownDirectiveHandler, loadTranslation, islandDirective, hashFilePath, getTranslation, getSourceLineInfo, generateTemplatesDocs, generateDocs, generateDirectivesDocs, generateComponentsDocs, generateComponentDoc, formatDocsAsMarkdown, formatDocsAsJson, formatDocsAsHtml, findComponentFiles, fileExists, extractVariables, extractComponentProps, extractComponentDescription, evaluateExpression, evaluateAuthExpression, escapeHtml, docsCommand, defaultFilters, defaultConfig, src_default as default, createTranslateFilter, createStreamRenderer, createDetailedErrorMessage, config, checkCache, cacheTemplate, buildWebComponents, applyFilters };