@stacksjs/stx
Version:
A Bun plugin that allows for using Laravel Blade-like syntax.
398 lines (395 loc) • 12.7 kB
JavaScript
// @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
};