UNPKG

@uswds/uswds

Version:

Open source UI components and visual style guide for U.S. government websites

197 lines (175 loc) • 6.39 kB
/** * Webpack loader that compiles `.twig` files into a render function. * * Adapted from `twigjs-loader` (https://github.com/megahertz/twigjs-loader), * which is itself based on `zimmo-be/twig-loader`. MIT-licensed (see below). * Vendored here to remove a dependency that is no longer actively maintained. * * --- * * MIT License * * Copyright (c) 2015 Zimmo.be * Copyright (c) 2018 Alexey Prokhorov * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * --- * * Behavior is intended to be equivalent to the original at the default * configuration: each `.twig` module exports `function(context) { return html }`. * The Express integration and the configurable `renderTemplate` option from * the original have been removed; neither was used by this repo. * * Dependency resolution (for `{% include %}`, `{% extends %}`, `{% embed %}`, * `{% import %}`, `{% from %}`) goes through Webpack's resolver, so loader * `resolve.alias` entries (e.g. `@components`, `@templates`) work as expected. */ const path = require("path"); const Twig = require("twig"); // Disable Twig's internal template cache. Webpack handles caching at the // module level; leaving Twig's cache on causes "There is already a template // with the ID" errors during HMR. Twig.cache(false); module.exports = function twigLoader(source) { const callback = this.async(); let template; try { template = Twig.twig({ allowInlineIncludes: true, data: source, id: makeTemplateId(this, this.resourcePath), path: this.resourcePath, rethrow: true, }); } catch (err) { return callback(err); } compile(this, template) .then((output) => callback(null, output)) .catch((err) => callback(err)); }; async function compile(loaderApi, template) { const dependencies = []; await each(template.tokens, (token) => processToken(loaderApi, token, dependencies), ); const twigData = { allowInlineIncludes: true, data: template.tokens, id: template.id, rethrow: true, }; const dependenciesString = [...new Set(dependencies)] .sort() .map((d) => `require(${JSON.stringify(d)});`) .join("\n"); return renderTemplate(twigData, dependenciesString, loaderApi.hot); } async function processToken(loaderApi, token, dependencies) { if (token.type !== "logic" || !token.token.type) { return; } switch (token.token.type) { case "Twig.logic.type.block": case "Twig.logic.type.if": case "Twig.logic.type.elseif": case "Twig.logic.type.else": case "Twig.logic.type.for": case "Twig.logic.type.spaceless": case "Twig.logic.type.macro": case "Twig.logic.type.apply": case "Twig.logic.type.setcapture": { await each(token.token.output, (t) => processToken(loaderApi, t, dependencies), ); break; } case "Twig.logic.type.extends": case "Twig.logic.type.include": { await each(token.token.stack, (t) => processDependency(loaderApi, t, dependencies), ); break; } case "Twig.logic.type.embed": { await each(token.token.output, (t) => processToken(loaderApi, t, dependencies), ); await each(token.token.stack, (t) => processDependency(loaderApi, t, dependencies), ); break; } case "Twig.logic.type.import": case "Twig.logic.type.from": { // `_self` is a special token meaning "macros from this same file"; // there is no external file to resolve. if (token.token.expression !== "_self") { await each(token.token.stack, (t) => processDependency(loaderApi, t, dependencies), ); } break; } default: // Other token types have no nested template references. break; } } async function processDependency(loaderApi, token, dependencies) { const absolutePath = await resolveModule(loaderApi, token.value); dependencies.push(token.value); // Rewrite the token's reference to a stable, root-relative id so Twig's // runtime lookup matches the id emitted by the dependency module. token.value = makeTemplateId(loaderApi, absolutePath); loaderApi.addDependency(absolutePath); } async function each(arr, callback) { if (!Array.isArray(arr)) return; await Promise.all(arr.map(callback)); } function makeTemplateId(loaderApi, absolutePath) { const root = loaderApi.rootContext || process.cwd(); return path.relative(root, absolutePath).split(path.sep).join("/"); } function resolveModule(loaderApi, modulePath) { return new Promise((resolve, reject) => { loaderApi.resolve(loaderApi.context, modulePath, (err, result) => { if (err) reject(err); else resolve(result); }); }); } /** * Generates the JS module that Webpack emits for each .twig file. * `isHot` disables Twig's runtime cache on rebuild so HMR doesn't trip the * "duplicate template id" error. */ function renderTemplate(twigData, dependencies, isHot) { const hmrFix = isHot ? '\nrequire("twig").cache(false);' : ""; return ` ${dependencies}${hmrFix} var twig = require("twig").twig; var tpl = twig(${JSON.stringify(twigData)}); module.exports = function(context) { return tpl.render(context); }; module.exports.id = ${JSON.stringify(twigData.id)}; module.exports.default = module.exports; `.replace(/^\s+/gm, ""); }