Compiler
The Marko compiler tries its best to get out of your way and for the most part you will only see references to it in the various Marko ecosystem plugins for tools like webpack, Rollup, and others.
The compiler also provides a series of hooks you can tap into to bend the Marko language to your will.
Note: It is best to use existing official plugins, and the standard tag library when possible.
Compile API
The compile API is relatively straightforward and always you to take any Marko file and turn it into executable JavaScript.
import * as compiler from "@marko/compiler"; type CompileOptions = typeof import("@marko/compiler/src/config.js"); type CompileResult = { meta: Record<string, unknown>; // Meta data gathered while compiling. map?: SourceMap; // A sourcemap. code: string; // The translated code. };
compiler.configure(options: CompileOptions)
The configure
API will override the default compiler options. For the list of options (applicable to all compile*
functions) see the source code.
compiler.configure({ output: "dom" });
compiler.compile(src: string, filename: string, options?: CompileOptions): Promise<CompileResult>
compiler.compileSync(src: string, filename: string, options?: CompileOptions): CompileResult
Both the compile
and compileSync
APIs will translate the provided Marko source code into JavaScript. The only difference between the two (as the names likely suggest) is that compile
will use async APIs under the hood while compileSync
will use sync APIs.
const asyncResult = await compiler.compile( "<h1>Hello!</>", "./src/index.marko", { modules: "cjs" } ); const syncResult = compiler.compileSync("<h1>Hello!</>", "./src/index.marko", { modules: "cjs" });
compiler.compileFile(filename: string, options?: CompileOptions): Promise<CompileResult>
compiler.compileFileSync(filename: string, options?: CompileOptions): CompileResult
compileFile
and compileFileSync
act the same as their counterparts compile
and compileSync
with the exception being that you do not need to pass in the source content. Instead, these APIs will load the file from disk for you automatically.
const asyncResult = await compiler.compileFile("./src/index.marko", { modules: "cjs" }); const syncResult = compiler.compileFileSync("./src/index.marko", { modules: "cjs" });
Hooks
The Marko compiler runs through a series of stages to produce the final JavaScript output. These stages are intended for different aspects of processing the template and can be hooked into using marko.json
configuration.
All compiler hooks must export a visitor which will receive a babel NodePath with a MarkoTag
node.
The hook will also receive a types
object that matches the @babel/types API extended with the Marko AST types. You can also get a reference to this by importing { types }
from the @marko/compiler
module.
Here is an example hook:
module.exports = (tag, t) => { if (t.isStringLiteral(tag.node.name)) { console.log(`Found a tag called ${tag.node.name.value}`); tag.remove(); } };
Hooks can also export an enter
(alias of default
) and an exit
function. These map to @babel/traverse's enter
and exit
methods.
Parse
The first step to Marko's compilation is to take the raw text of your Marko template and convert it into an "Abstract Syntax Tree". If you've not heard the term before, put simply it is just an object representation of your code.
<h1>Hello!</h1>
h1 -- Hello!
Will roughly become
{ "type": "MarkoTag", "name": { "type": "StringLiteral", "value": "h1" }, "body": { "type": "MarkoTagBody", "body": [ { "type": "MarkoText", "value": "Hello!" } ] } }
This might look a bit verbose, but we are aiming for completeness, not terseness in this output.
Marko takes a two-step parsing approach to remain flexible with the ever-changing syntax of JavaScript. The first pass of parsing happens in our very own htmljs-parser, which understands the HTML parts of your template.
For JavaScript expressions, Marko defers to @babel/parser. The Marko AST above is a superset of what would be returned from @babel/parser
.
To hook into the parse
stage you can use the parse
option in the marko.json
file. The parse
hook deviates from the rest of the compiler hooks in that it does not support the enter
& exit
API and you must return a replacement AST node.
Migrate
That's right, Marko has _ first-class_ support for migrations. This compiler hook allows for translating outdated APIs into their modern counterparts, leaving the rest of the compilation non the wiser. These migrations run automatically in the background and can be written to disk when users are ready by running the @marko/migrate
CLI command.
To hook into the migrate
stage you can use the migrate
option in the marko.json
file.
Transform
The transform stage of the compiler is meant for userland transformations of Marko code, into other Marko code. Think of it like babel.transform for Marko templates. At this stage, you are given a fully parsed and migrated AST to do what you will with.
To hook into the transform
stage you can use the transform
option in the marko.json
file.
Translate
Finally, we have the translation stage. This stage is Marko's "Rosetta Stone" and is responsible for turning your beautiful Marko code into the optimized JavaScript you'd rather avoid writing.
To hook into the translate
stage you can use the translate
option in the marko.json
file.
Utilities
The @marko/babel-utils
package exposes a handful of utilities for performing various tasks on the Marko AST.
Marko AST
You can check out the AST extensions that Marko makes in the source code. For AST creation and assertion utilities you can also import Marko's superset of @babel/types
through the compiler via import { types } from "@marko/compiler"
.
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.