diff --git a/packages/share-theme/scripts/esbuild-ejs-plugin.ts b/packages/share-theme/scripts/esbuild-ejs-plugin.ts index 1b74bc283..56ba351e2 100644 --- a/packages/share-theme/scripts/esbuild-ejs-plugin.ts +++ b/packages/share-theme/scripts/esbuild-ejs-plugin.ts @@ -1,21 +1,85 @@ import { readFile } from 'fs/promises'; -import { compile } from 'ejs'; +import { compile, type Options } from 'ejs'; +import { resolve, dirname, basename } from 'path'; export default function esbuildPluginEjs(options = {}) { - return { - name: 'ejs', - setup(build) { - build.onLoad({ filter: /\.ejs$/ }, async args => { - const template = await readFile(args.path, 'utf8') - const ejsOptions = { - ...options, - client: true, - strict: true, - compileDebug: false } - const generator = compile(template, ejsOptions) - const contents = `module.exports = ${generator.toString()};` - return { contents, loader: 'js' } - }) + return { + name: 'ejs', + setup(build) { + build.onLoad({ filter: /\.ejs$/ }, async args => { + const ejsOptions: Options = { + ...options, + client: true, + strict: true, + compileDebug: false + } + + const contents: string[] = []; + contents.push(`const includeMap = {}`); + contents.push(` + function __include(name, ...args) { + return includeMap[name](...args); + } + `); + + let main; + + // Compile the subtemplates. + const subtemplates = await collectTemplateTree(args.path); + for (const [ path, subtemplate ] of Object.entries(subtemplates)) { + const functionName = basename(path).split(".")[0]; + const isMain = (path === args.path); + + const generator = compile(subtemplate, ejsOptions); + const functionOutput = generator.toString().split("\n"); + + if (isMain) { + functionOutput[0] = functionOutput[0].replace(/^function anonymous/, `module.exports = function`); + } else { + functionOutput[0] = functionOutput[0].replace(/^function anonymous/, `includeMap["${functionName}"] = function`); + } + + // Inject include function. + functionOutput[2] = `include = __include;\n${functionOutput[2]}`; + + if (isMain) { + main = functionOutput.join("\n"); + } else { + contents.push(functionOutput.join("\n")); + } + } + + // Compile the rest. + if (!main) { + throw new Error("Missing main entry point"); + } + contents.push(main); + + return { contents: contents.join("\n"), loader: 'js' } + }) + } } - } +} + + +const includeRegex = /<%-?\s*include\((['"`])(.+?)\1\s*(?:,[^)]+)?\)\s*-?%>/g; + +async function collectTemplateTree(filePath, seen: Record = {}) { + if (seen[filePath]) return; + + const source = await readFile(filePath, 'utf8'); + seen[filePath] = source; + + const dir = dirname(filePath); + + const matches = [...source.matchAll(includeRegex)]; + for (const match of matches) { + const includePath = match[2]; + + // Add .ejs extension if needed + const resolvedPath = resolve(dir, includePath.endsWith('.ejs') ? includePath : includePath + '.ejs'); + await collectTemplateTree(resolvedPath, seen); + } + + return seen; }