WoltLabCompiler
A compiler for generating WoltLab-Package .tar
Archives and other WoltLab-Package Components
Table of Contents
About WoltLabCompiler
Writing packages for WoltLab Suite Core ain't easy. Normally, you have to work yourself through a whole bunch of docs helping you to create an exhausting amount of repetitive .xml
-files.
There are a few design limitations WoltLab packages are bound to:
- The
.xml
-files mentioned before are quite redundant highly increasing the risk of doing typos - Options and emojis are strictly separated from their corresponding translations (such as the human readable name and description)
- The
.tar
-archive has to be created manually - There is no such thing like error detection, you have to try to install your package in order to see whether you did any error
WoltLabCompiler
is an attempt to try to fix these issues and make creating packages for WoltLab Suite Core easier and more accessible and understandable. It provides components for generating WoltLab packages or single components such as translation files, metadata files or themes. Furthermore, this package provides extensive documentation comments making editing packages easier.
In order to create this package, WoltLab's official documentations were used. You might want to have a look at it in case you're wondering how you were supposed to write packages originally.
Installing WoltLabCompiler
WoltLabCompiler
can be installed using the npm
-cli:
npm install --save @manuth/woltlab-compiler
Using WoltLabCompiler
Using WoltLabCompiler, you can create metadata representing your package's component and compile this metadata to a WoltLab package.
In order to get your package compiled following steps are necessary:
- Setting up a compiler
- Filling the compiler with metadata
- Setting the path to write the output file to
- Running the compiler
- Profit!
Following piece of code creates a small package providing new dummy options for WoltLab's control panel:
import { join } from "node:path";
import { ACPOptionInstruction, InvariantCultureName, OptionType, Package, PackageCompiler } from "@manuth/woltlab-compiler";
(async () =>
{
let compiler = new PackageCompiler(
new Package(
{
Identifier: "com.example.my-package",
DisplayName: {
[InvariantCultureName]: "My Package",
de: "Mein Paket"
},
Version: "0.0.1 Beta 1",
InstallSet: {
Instructions: [
new ACPOptionInstruction(
{
FileName: "acpOptions.xml",
Nodes: [
{
Name: "my-package",
Parent: {
Name: "general"
},
Item: {
DisplayName: {
de: "Mein Paket",
en: "My Package"
},
Options: [
{
Name: "enabled",
DisplayName: {
de: "Aktiviert",
en: "Enabled"
},
Description: {
de: "Aktiviert `Mein Paket`",
en: "Enables `My Package`"
},
Type: OptionType.CheckBox
}
]
}
}
]
})
]
},
ConflictingPackages: [
{
Identifier: "com.woltlab.wcf",
Version: "6.0.0 Alpha 1"
}
]
}));
compiler.DestinationPath = join("my-package.tar");
await compiler.Execute();
})();
This will create a .tar
-archive which is ready to install in WoltLab Suite Core.
The .tar
-archive contains all the files which you originally would have had to write manually:
- A
package.xml
-file containing the package's metadata de.xml
anden.xml
-files containing the translated nodes (such asDisplayName
andDescription
)- An
acpOptions.xml
-file containing the metadata of the options
Quick Start
In order to get started as quickly as possible, you might want to create a ready-to-use workspace using Yeoman:
npm install --global yo @manuth/generator-wsc-package
yo @manuth/wsc-package
This tool will help you create basic code for easily creating a WoltLab package.
However, if you'd like to find out more on how WoltLabCompiler works, you might want to continue reading a little further.
Core Concept
In order to use the WoltLabCompiler properly, it's important to get a grasp of its core concept. Said core concept is explained briefly in the further paragraphs.
Package
A package is an extension which can be installed in WoltLab Suite Core's admin panel.
Packages consist of metadata and a set of instructions which are executed during the installation. Furthermore, packages can provide instruction-sets for updating the package from a specific version.
Instruction
An instruction is a step which is executed while installing or updating a package.
Compiler
Compilers provided by WoltLabCompiler
are components for generating files.
To name a few, there is a compiler which generates a directory containing language files, there is a compiler for generating the package's metadata (package.xml
), a compiler for generating the options-file etc.
The only compiler you might ever be using might be the PackageCompiler
for generating a WoltLab package or the ThemeCompiler
for compiling WoltLab themes.
However, you can even generate individual files in case you want to take care of creating the .tar
file yourself:
import { join } from "node:path";
import { ACPOptionFileCompiler, ACPOptionInstruction } from "@manuth/woltlab-compiler";
(async () =>
{
let compiler = new ACPOptionFileCompiler(
new ACPOptionInstruction(
{
FileName: "acpOption.xml",
Nodes: [
{
Name: "example",
Parent: {
Name: "general"
}
}
]
}));
compiler.DestinationPath = join(compiler.Item.FileName);
await compiler.Execute();
})();
Localizations
Some of the metadata, such as descriptions, is localizable. You can provide localizations using an object-hash with a key for each locale and its corresponding translation:
let localization = {
en: "Main Form",
de: "Hauptformular"
}
In some rare cases you can provide a fallback value which will be shown if the translation isn't available in WoltLab's configured language:
import { InvariantCultureName } from "@manuth/woltlab-compiler";
let localization = {
[InvariantCultureName]: "Main Form",
en: "Main Form",
de: "Hauptformular"
}
Please notice, that providing a fallback value most commonly isn't possible due to WoltLab's specifications.
The Basics
With the previously mentioned core concept in mind, the first thing you might want to do is creating a piece of code for creating a dummy package:
import { join } from "node:path";
import { Package, PackageCompiler } from "@manuth/woltlab-compiler";
(async () =>
{
let compiler = new PackageCompiler(
new Package(
{
Identifier: "MyPackage",
DisplayName: {
[InvariantCultureName]: "My Package"
},
Version: "0.0.1 Alpha 1",
InstallSet: {
Instructions: []
}
}));
compiler.DestinationPath = join("my-package.tar");
})();
Adding Instructions
All that's left to do is adding instructions to the installation- and update-queue in order to add some functionality to your package.
Commonly, instructions accept a FileName
-property which allows you to specify the name of the file which ends up in the resulting package's .tar
archive.
This chapter provides a brief overview of the most important available instructions.
Control Panel Settings
The ACPOptionInstruction
allows you to add options to the administration control panel.
The constructor accepts a single argument containing metadata for the options.
The most important properties are listed here:
FileName
:
The name of the file to add to the.tar
-archiveNodes
:
The categories to provide. Each have following options:Name
:
The name of the category. This name is used to identify the category.Parent
:
Information about a pre-existing parent categoryNodes
:
A set of sub-categoriesItem
:
The metadata of the category:DisplayName
:
The human-readable name of the category. This option is localizable.Description
:
The description of the category. This option is localizable.Options
:
The options of the category.
Example:
import { ACPOptionInstruction, OptionType } from "@manuth/woltlab-compiler";
new ACPOptionInstruction(
{
FileName: "acpOptions.xml",
Nodes: [
{
Name: "example",
Parent: {
Name: "general"
},
Item: {
DisplayName: {
en: "This is an example",
de: "Das ist ein Beispiel"
},
Options: [
{
Name: "enabled",
DisplayName: {
en: "Enabled",
de: "Aktiviert"
},
Type: OptionType.CheckBox
}
]
}
}
]
});
Note:
In most cases, you might want to add your categories and options to an already existing category. In order to do so you must pass it's name to theParent
property.The example shown previously adds the category
example
(and all its options and sub-categories) to an already existing category namedgeneral
.
Files and Templates
In most cases you might want to add a few templates and files to the packages which are uploaded to WoltLab Suite Core during the installation.
The ApplicationFileSystemInstruction
allows you to upload files while the ACPTemplateInstruction
and the TemplateInstruction
allow you to upload templates for the admin control panel or the frontend.
All instructions mentioned before are used in the same manner and thus accept the same properties:
FileName
:
The name of the file to compress and save the files to. If this property is omitted, a default filename will be used based on the specifiedApplication
and/or theSource
properties.Application
:
The abbreviated name of the application to upload the files/templates to (e.g.wbb
,gallery
,wcf
etc.). If you omit this property, WoltLab Suite Core will determine an application to upload the files/templates to automatically. Please refer to WoltLab's docs.Source
:
The path to the directory to load the files/templates from.
Example:
import { join } from "node:path";
import { ACPTemplateInstruction, TemplateInstruction, ApplicationFileSystemInstruction } from "@manuth/woltlab-compiler";
new ACPTemplateInstruction(
{
FileName: "acpTemplates.tar",
Application: "wcf",
Source: join("..", "assets", "acpTemplates")
});
new TemplateInstruction(
{
Application: "wcf",
Source: join("..", "assets", "templates")
});
new ApplicationFileSystemInstruction(
{
Source: join("..", "assets", "files")
});
Note:
In some rare cases - especially when writing PHP files - you might want to have a few parts of your files replaced with metadata from your package. This might especially be useful if you're accessing ACP options from your PHP scripts and you're likely to rename these options at some point.The
SelfContainedPHPInstruction
,ACPTemplateInstruction
,TemplateInstruction
,ApplicationFileSystemInstruction
andSQLInstruction
preprocess the specified files using ejs.You can access the instruction's metadata using
<%= Item %>
or add anID
to your most important components and access them in ejs-snippets using<%= $("MyID") %>
.Example:
index.ts:
Show Verbose Code
import { join } from "node:path"; import { ACPOptionInstruction, ApplicationFileSystemInstruction, Package, PackageCompiler, TemplateInstruction } from "@manuth/woltlab-compiler"; (async () => { let compiler = new PackageCompiler( new Package( { Identifier: "com.example.test", DisplayName: { [InvariantCultureName]: "My Test Package" }, InstallSet: { Instructions: [ new ACPOptionInstruction( { ID: "ExampleCategory", Name: "example", Parent: { Name: "general" }, Item: { DisplayName: { en: "Example Category" } Options: [ { ID: "ExampleEnabledOption", Name: "example_enabled", DisplayName: { en: "Enabled" } } ] } }), new TemplateInstruction( { Source: join("templates") }), new ApplicationFileSystemInstruction( { Source: join("files") }) ] } })); compiler.DestinationPath = join("test.tar"); await compiler.Execute(); })();./templates/example.tpl
<p> Please enable the "{lang}wcf.acp.option.<%= $("ExampleEnabledOption").Name %>{/lang}" option in the "{lang}wcf.acp.option.category.<%= $("ExampleCategory").FullName %>{/lang}" in order to use <%= Item.Package.DisplayName["inv"] %>. </p>Output:
<p> Please enable the "{lang}wcf.acp.option.enabled{/lang}" option in the "{lang}wcf.acp.option.category.general.my-package{/lang}" in order to use My Package. </p>./files/example.php
<?php namespace wcf\system\option\example; { /** * Handles options related to My Test Package. */ class OptionHandler extends \wcf\system\option\OptionHandler { /** * @inheritDoc */ public function validate() { $errors = parent::validate(); if ($this->getOption("<%= $('ExampleEnabledOption').Name %>")["value"] === 1) { // My Validation Code } } } } ?>Output:
// [...] if ($this->getOption("example_enabled")["value"] === 1) { // [...] } // [...]
Themes
One more goal of WoltLabCompiler
is to make creating themes easier. Instead of declaring scss-variables in an .xml
-file, said variables can now be written in plain .scss
.
The ThemeInstruction
accepts an object-hash containing the following properties:
Theme
:
Provides the theme's metadata:FileName
:
The name of the.tar
-archive containing the theme which will be added to your package's.tar
. If this option is omitted, the filename will be determined according to the specifiedName
.Name
:
The name of the theme which is used for determining theFileName
DisplayName
:
The human-readable name of the theme. This option is localizable and accepts a fallback value using theInvariantCultureName
.Thumbnail
,HighResThumbnail
,CoverPhoto
:
Declares the location of miscellaneous theme assets.Source
: The path to load the asset fromFileName
: The name the file should have inside the theme's.tar
-file
Images
:
Declares the location of the pictures provided by the theme.Source
: The path to the directory to load the pictures from.DestinationRoot
: The name of the directory on the webserver to upload the pictures toFileName
: The name the picture-archive should have inside the theme's.tar
Variables
:
A set of the theme's.scss
-variables and their corresponding values. Unknown variables will end up in WoltLab'sscssOverride
theme-variable.CustomScssFileName
:
The path to a.scss
-file which contains the theme'sscss
-code. The code of the specified file will end up in WoltLab'sindividualScss
theme variable.ScssOverrideFileName
:
If you prefer managing your theme-variables in a.scss
-file rather than theVariables
-property, you can specify the path to a.scss
-file which contains theme-variables. Unknown variables will end up in WoltLab'sscssOverride
theme-variable.VariableFileName
:
In case you prefer to manage your theme-variables in a.json
file rather than theVariables
-property, you can specify the path to a.json
-file which provides a set of variable-names and their corresponding values. Unknown variables will end up in WoltLab'sscssOverride
theme-variable.
The priorities of the variables are as followed:
- Variable-file (as specified per
VariableFileName
) - The
.scss
-overrides (as specified perScssOverrideFileName
) - The plain variables specified using the
Variables
-properties
Example:
Show Verbose Code
index.ts
import { join } from "node:path"; import { Package, PackageCompiler } from "@manuth/woltlab-compiler"; (async () => { let compiler = new PackageCompiler( new Package( { Identifier: "com.example.my-theme", DisplayName: { [InvariantCultureName]: "My Theme" }, Version: "0.0.1 Beta 1", InstallSet: { Instructions: [ new ThemeInstruction( { Theme: { Name: "my-theme", DisplayName: { [InvariantCultureName]: "My Theme" }, CoverPhoto: join("cover.jpg"), // Loads the picture from ./cover.jpg Thumbnail: join("thumbnail.png"), // Loads the thumbnail from ./thumbnail.png HighResThumbnail: join("hq-thumbnail.png"), // Loads the hq thumbnail from ./hq-thumbnail.png Variables: { wcfFontSizeDefault: "1px" }, CustomScssFileName: join("style.scss"), // Loads the custom scss code from ./style.scss ScssOverrideFileName: join("overrides.scss"), // Loads variables from ./overrides.scss VariableFileName: join("variables.json"), Images: { Source: join("images") // Loads the images from ./images } } }) ] } })); })();./style.scss
:root { background: red; }./overrides.scss
$wcfHeaderBackground: green;./variables.json
{ "wcfLayoutMinWidth": "2000px" }
There are many more features, compilers and components you may want to work with.
Have a look at the JSDoc-comments which are displayed automatically when working with an extended code-editor.