Vanilla's Frontend Build System - HL Vanilla Community
<main> <article class="userContent"> <div class="embedExternal embedImage display-large float-none"> <div class="embedExternal-content"> <a class="embedImage-link" href="https://us.v-cdn.net/6030677/uploads/I4VEDUL7H56Q/microsoftteams-image-288-29.png" rel="nofollow noreferrer noopener ugc" target="_blank"> <img class="embedImage-img" src="https://us.v-cdn.net/6030677/uploads/I4VEDUL7H56Q/microsoftteams-image-288-29.png" alt="MicrosoftTeams-image (8).png" height="108" width="1356" loading="lazy" data-display-size="large" data-float="none"></img></a> </div> </div> <p>Vanilla’s frontend scripts use a single global build process. This is used for all internal Javscript, both in core and addons.</p><h2 data-id="what-does-it-do">What does it do?</h2><p>The included build process uses Typescript and Webpack to bundle typescript files to into javascript bundles.</p><p>Every addon in your current vanilla project containing entries will get built. Currently that means bundling scripts from the following addons:</p><ul><li><code class="code codeInline" spellcheck="false" tabindex="0">vanilla</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">dashboard</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">rich-editor</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">foundation</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">subcommunities</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">knowledge</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">vanillaanalytics</code></li><li>Various others.</li></ul><p>The outputted bundles will automatically be loaded by Vanilla into the page if their addon is enabled.</p><h2 data-id="prerequisites">Prerequisites</h2><p>Node 10+ and Yarn are prerequisites to run this build tool. See our <a href="https://success.vanillaforums.com/kb/articles/155-local-setup-quickstart#prerequisites" rel="nofollow noreferrer ugc">Local Setup Quickstart</a> for setup instructions.</p><h2 data-id="composer-post-install">Composer post-install</h2><p>The build is run automatically in a <code class="code codeInline" spellcheck="false" tabindex="0">post-install</code> hook of composer. This is to ensure a <code class="code codeInline" spellcheck="false" tabindex="0">composer install</code> provides a fully functioning vanilla setup. The equivalent command will be run:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">yarn install --pure-lockfile yarn build -i </pre><p>There are a few environmental variables that can affect how this composer post-install script is started.</p><h3 data-id="vanilla_build_disable_auto_build">VANILLA_BUILD_DISABLE_AUTO_BUILD</h3><p>Setting this environmental variable will disable automatic building after a composer install. So if you have a post-checkout hook and don’t want to run the build on every checkout you can could run your install like this:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">VANILLA_BUILD_DISABLE_AUTO_BUILD=true composer install </pre><h3 data-id="vanilla_build_node_args">VANILLA_BUILD_NODE_ARGS</h3><p>The value of this environmental variable will be passed as arguments to the nodejs process used by the build script.</p><pre class="code codeBlock" spellcheck="false" tabindex="0"># Restrict the node process to 512Mb of RAM. VANILLA_BUILD_NODE_ARGS="--max-old-space-size=512" composer install </pre><p>If you are running out of error you might want to increase this memory limit.</p><h3 data-id="vanilla_build_low_memory">VANILLA_BUILD_LOW_MEMORY</h3><p>This is a boolean flag that you can set to build in a low memory mode. The build will take longer, but consume less memory.</p><pre class="code codeBlock" spellcheck="false" tabindex="0"># Build in low memory mode. VANILLA_BUILD_LOW_MEMORY=true </pre><h2 data-id="usage">Usage</h2><pre class="code codeBlock" spellcheck="false" tabindex="0"># From the root of your vanilla installation yarn build <options> yarn build:<mode> <options> # Modes yarn build yarn build:dev yarn build:analyze # Short and long version of the different params yarn build:dev --verbose yarn build:dev -v yarn build --verbose --fix yarn build -vf </pre><h2 data-id="build">build</h2><p>This is the default production build. It will generate full sourcemaps and output production ready javascript to the disk inside of your repos. It is:</p><ul><li>Required to ship production code.</li><li>Slow.</li><li>Accurate.</li></ul><h2 data-id="build%3Adev">build:dev</h2><p>Development builds <em>do not</em> write javascript to the disk. Instead the typescript files from all of your enabled addons and will be build into a single file that is kept in memory only and served through a local web server.</p><p>There are a few benefits to this approach.</p><ul><li>Significantly faster rebuilds (less than a second normally).</li><li>“Hot” automatic reloading of parts of the page. With React components a full page refresh is not required. Instead only the changed components will be refreshed.</li></ul><p>Using the development build requires you to set the following configuration value:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">$Configuration['HotReload']['Enabled'] = true; </pre><p>Setting this config value will instruct your Vanilla installation to load its built javascript from the development build’s web-server instead of the pre-built files. Don’t forget to turn this off when you’re done!</p><h2 data-id="build%3Aanalyse">build:analyse</h2><p>This command runs a production build then uses <code class="code codeInline" spellcheck="false" tabindex="0">webpack-analyze</code> to open a local webserver with details about:</p><ul><li>What size your bundles are (in original, parsed, and gzipped size).</li><li>Where that size comes from.</li><li>How large files are.</li><li>Which files are in which bundles.</li></ul><h3 data-id="options">Options</h3><p>The following flags have a long option and name a short alias. Use whichever your prefer.</p><h4 data-id="verbose-or-v">–verbose or -v</h4><p>Print additional output to your console.</p><h4 data-id="fix-or-f">–fix or -f</h4><p>Automatically fix styling and fixable lint errors in all built source files. Do <strong>not</strong> use this at the same time as an IDE that formats on save. Your IDE will have conflicts with the build tool as they both attempt to write to the same file.</p><h2 data-id="source-files-and-entries">Source files and entries</h2><p>All source files <strong>MUST</strong> be typescript files with an extension of <code class="code codeInline" spellcheck="false" tabindex="0">.ts</code> or <code class="code codeInline" spellcheck="false" tabindex="0">.tsx</code> and reside in the <code class="code codeInline" spellcheck="false" tabindex="0">src/scripts</code> directory of an addon.</p><p>Entries <strong>MUST</strong> be placed directly in the <code class="code codeInline" spellcheck="false" tabindex="0">src/scripts/entries</code> directory of an addon. Adding an entry of a given name will create an entry of that type. Currently Vanilla defines 3 common entries: <code class="code codeInline" spellcheck="false" tabindex="0">forum</code>, <code class="code codeInline" spellcheck="false" tabindex="0">admin</code>, and <code class="code codeInline" spellcheck="false" tabindex="0">knowledge</code>.</p><p>Creating a <code class="code codeInline" spellcheck="false" tabindex="0">common</code> entry will apply to all 3 sections at once. This can be used at the same time as the other sections.</p><p>This means an entry for one of those sections would be one of the following files.</p><ul><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/forum.ts</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/admin.ts</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/knowledge.ts</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/common.ts</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/forum.tsx</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/admin.tsx</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/knowledge.tsx</code></li><li><code class="code codeInline" spellcheck="false" tabindex="0">/plugins/MY_PLUGIN/src/scripts/entries/common.tsx</code></li></ul><p>Every other file may be imported from one of these entries.</p><h3 data-id="dynamic-entries">Dynamic entries</h3><p>Anytime you have secondary content of a significant size that is not the primary content of a page it should be dynamically imported. This will split everything from that import down the chain into its own javascript bundle. If the import statement is reached, that separate bundle will be dynamically loaded by the client, and the imported file’s exports returned in a Promise.</p><p>Rich Editor is a great example of this. It:</p><ul><li>Only loads under certain conditions (signed in user, posting permissions, on a comment/posting page).</li><li>Very large (~400 Kb).</li></ul><p>So in the <code class="code codeInline" spellcheck="false" tabindex="0">plugins/rich-editor/src/scripts/entries/forum.ts</code> you can find code similar to the following:</p><pre class="code codeBlock" spellcheck="false" tabindex="0">async function startEditor() { if (pageNeedsRichEditor()) { const mountEditorModule = await import("./mountEditor" /* webpackChunkName="chunks/mountEditor" */) const mountEditor = mountEditorModule.default; mountEditor(getMountPoint()); } } </pre><p>There are a few things to decouple here.</p><ol><li>The <code class="code codeInline" spellcheck="false" tabindex="0">import()</code> function returns a Promise of module. It is an asyncrounous operation and must be handled as such.</li><li>Default exports get put on a named property <code class="code codeInline" spellcheck="false" tabindex="0">default</code> of the imported module. Named exports will be put on a property of their name. See <a href="https://webpack.js.org/api/module-methods/#import-" rel="nofollow noreferrer ugc">webpack’s import() documentation</a> for more details.</li><li>You <strong>MUST</strong> provide a <code class="code codeInline" spellcheck="false" tabindex="0">webpackChunkName</code> property. Omitting it will result in a chunk named <code class="code codeInline" spellcheck="false" tabindex="0">0.min.js</code> or <code class="code codeInline" spellcheck="false" tabindex="0">1.min.js</code> in the root the sections build directory where 0 or 1 will be a automatically incrementing integer. The files will still be loaded, but providing a name allows for easier viewing of what scripts are loaded in the page.</li></ol><h2 data-id="site-sections">Site Sections</h2><p>Every addon may offer entrypoints for different “sections” of the site. These will get loaded based off the javascript files requested from the <code class="code codeInline" spellcheck="false" tabindex="0">WebpackAssetProvider::getScripts(string $section)</code> or automatically depending on the return of a <code class="code codeInline" spellcheck="false" tabindex="0">Page::getAssetSection()</code>.</p><p><strong><code class="code codeInline" spellcheck="false" tabindex="0">forum</code></strong><strong> entries</strong></p><p>Forum entries are loaded in what would be considered the “frontend” of the site. That is anything using the <code class="code codeInline" spellcheck="false" tabindex="0">default</code> master view (currently <code class="code codeInline" spellcheck="false" tabindex="0">default.master.tpl</code>).</p><p><strong><code class="code codeInline" spellcheck="false" tabindex="0">admin</code></strong><strong> entries</strong></p><p>Admin entries are for the administrative dashboard of the site. That is anything using the <code class="code codeInline" spellcheck="false" tabindex="0">admin</code> master view (currently <code class="code codeInline" spellcheck="false" tabindex="0">admin.master.tpl</code>).</p><p><strong>Additional entries</strong></p><p>If you wanted to create a entry for a new section (lets use <code class="code codeInline" spellcheck="false" tabindex="0">mySection</code> as an example) you would do the following:</p><ol><li>Create a file <code class="code codeInline" spellcheck="false" tabindex="0">src/scripts/entries/mySection.ts</code> or <code class="code codeInline" spellcheck="false" tabindex="0">src/scripts/entries/mySection.tsx</code> in your addon.</li><li>Run the build.</li><li>Call <code class="code codeInline" spellcheck="false" tabindex="0">$assetModel->getWebpackJsFiles('mySection')</code> and add the resulting script files to your page.</li></ol><h2 data-id="output-files">Output files</h2><p>The <code class="code codeInline" spellcheck="false" tabindex="0">WebpackAssetProvider</code> is responsible for gathering build files. You should not be referencing them directly as their locations may change in the future. Please use <code class="code codeInline" spellcheck="false" tabindex="0">WebpackAssetProvider</code> and use its methods instead.</p><h3 data-id="location">Location</h3><p>Output files are build into the <code class="code codeInline" spellcheck="false" tabindex="0">dist</code> directory. Each section get’s it own folder. The folder structure of a section looks like this:</p><p><strong>dist/forum</strong></p><pre class="code codeBlock" spellcheck="false" tabindex="0">runtime.min.js (Webpack runtime) vendors.min.js (vendor JS. Everything from node_modules) shared.min.js (Shared code from the `@library`) addons/* (Build entry points from addons. Eg. `addons/rich-editor.mins.js`, `addons/dashboard.min.js`) bootstrap.min.js (The script the fires the `onReady()` event.) async~someChunkName.min.js </pre><p>Each of these files has its own sourcemap file as well. The <code class="code codeInline" spellcheck="false" tabindex="0">async~</code> the chunks build from dynamic import statements.</p> </article> </main>