In the last article I told about the new Parcel bandler, which does not require configuration and is ready for battle immediately after installation. But what to do if suddenly there is not enough standard set of assets? The answer is simple - write your own plugin.
Let me remind you that the following assets are available out of the box:
To create our asset, we can choose two ways - use the existing one ( JSAsset , HTMLAsset , etc.), in which we can rewrite or add part of the logic, or write from scratch, using the Asset class as a basis.
As an example, I’ll tell you how the plugin for Pug was written.
First you need to figure out how Parcel works with plugins and what can they do at all?
When the bundler is initialized ( Bundler ), packages are searched in package.json, starting with parcel-plugin-
. Each bundle found by the package connects and calls the exported function, passing its context to it. In this function, we can register your asset.
Our asset should implement the following methods:
/** * AST */ parse(code: string): Ast /** * */ collectDependencies(): void /** * AST */ generate(): Object
You can also implement optional methods:
/** * AST */ pretransform(): void /** * AST */ transform(): void
There are several official packages for working with AST:
pug-load
- loads the text and gives it to the lexer and the parserpug-lexer
- parses text on tokenspug-parser
- turns an array of tokens into ASTpug-linker
- sticks together several ASTs, is needed for include
and extends
pug-walk
- allows you to walk on AST and modify itpug-ode-gen
- generates HTML using a javascript functionpug-runtime
- contains a wrap
function that allows you to wrap and execute the function returned from pug-ode-gen
Create the following project structure:
parcel-plugin-pug ├── package.json ├── src │ ├── PugAsset.ts │ ├── index.ts │ └── modules.d.ts ├── tsconfig.json └── tslint.json
The index.ts
file will be the entry point to our plugin:
export = (bundler: any) => { bundler.addAssetType('pug', require.resolve('./PugAsset')); bundler.addAssetType('jade', require.resolve('./PugAsset')); };
To work with an asset, we need the base class Asset
. Let's write a TypeScript binding for the modules we need:
declare module 'parcel-bundler/src/Asset' { class Asset { constructor(name: string, pkg: string, options: any); parse(code: string): any; addDependency(path: string, options: Object): any; addURLDependency(url: string): string; name: string; isAstDirty: boolean; contents: string; ast: any; options: any; dependencies: Set<Object>; } export = Asset; } declare module 'parcel-bundler/src/utils/is-url' { function isURL(url: string): boolean; export = isURL; } declare module 'pug-load' { class load { static string(str: string, options?: any): any; } export = load; } declare module 'pug-lexer' { class Lexer {} export = Lexer; } declare module 'pug-parser' { class Parser {} export = Parser; } declare module 'pug-walk' { function walkAST(ast: any, before?: (node: any, replace?: any) => void, after?: (node: any, replace?: any) => void, options?: any): void; export = walkAST; } declare module 'pug-linker' { function link(ast: any): any; export = link; } declare module 'pug-code-gen' { function generateCode(ast: any, options: any): string; export = generateCode; } declare module 'pug-runtime/wrap' { function wrap(template: string, templateName?: string): Function; export = wrap; }
The PugAsset.ts
file is our asset for converting template files into HTML.
import Asset = require('parcel-bundler/src/Asset'); export = class PugAsset extends Asset { public type = 'html'; constructor(name: string, pkg: string, options: any) { super(name, pkg, options); } public parse(code: string) { } public collectDependencies(): void { } public generate() { } };
Start by turning the template text into AST. As I said before, when a filer comes across a file, it tries to find its asset. If it was found, the call chain parse -> pretransform -> collectDependencies -> transform -> generate
. Our first step is to implement the parse
method:
public parse(code: string) { let ast = load.string(code, { // lex: lexer, // parse: parser, // , filename: this.name }); // AST ( include extends) ast = linker(ast); return ast; }
Next, we need to go through the constructed tree and find any elements that may contain links. The mechanism of operation is quite simple and was HTMLAsset
in the standard HTMLAsset
. The bottom line is to create a dictionary with HTML-site attributes that may contain links. When passing through the tree, you need to find the appropriate nodes and feed the contents of the attribute with a link to the addURLDependency
method, which will try to find the necessary asset depending on the file extension. If the asset is found, the method will return the new file name, simultaneously adding this file to the assembly tree (this is the way the nested conversion of other assets takes place). This name we need to substitute the old way. We also need to take into account the fact that we need to add all the included files ( include
and extends
) as dependencies of this asset, otherwise if we change the included or base file, we will not have to reassemble the entire template.
interface Dictionary<T> { [key: string]: T; } const ATTRS: Dictionary<string[]> = { src: [ 'script', 'img', 'audio', 'video', 'source', 'track', 'iframe', 'embed' ], href: ['link', 'a'], poster: ['video'] };
public collectDependencies(): void { walk(this.ast, node => { // , , if (node.filename !== this.name && !this.dependencies.has(node.filename)) { // this.addDependency(node.filename, { name: node.filename, // includedInParent: true // }); } // , if (node.attrs) { // for (const attr of node.attrs) { const elements = ATTRS[attr.name]; // - if (node.type === 'Tag' && elements && elements.indexOf(node.name) > -1) { // Pug URL , let assetPath = attr.val.substring(1, attr.val.length - 1); // assetPath = this.addURLDependency(assetPath); // - if (!isURL(assetPath)) { // Use url.resolve to normalize path for windows // from \path\to\res.js to /path/to/res.js assetPath = url.resolve(path.join(this.options.publicURL, assetPath), ''); } // attr.val = `'${assetPath}'`; } } } return node; }); }
The final touch is getting the final HTML. This is the responsibility of the generate
method:
public generate() { const result = generateCode(this.ast, { // compileDebug: false, // pretty: !this.options.minify }); return { html: wrap(result)() }; }
If you put it all together we get the following:
import url = require('url'); import path = require('path'); import Asset = require('parcel-bundler/src/Asset'); import isURL = require('parcel-bundler/src/utils/is-url'); import load = require('pug-load'); import lexer = require('pug-lexer'); import parser = require('pug-parser'); import walk = require('pug-walk'); import linker = require('pug-linker'); import generateCode = require('pug-code-gen'); import wrap = require('pug-runtime/wrap'); interface Dictionary<T> { [key: string]: T; } // A list of all attributes that should produce a dependency // Based on https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes const ATTRS: Dictionary<string[]> = { src: [ 'script', 'img', 'audio', 'video', 'source', 'track', 'iframe', 'embed' ], href: ['link', 'a'], poster: ['video'] }; export = class PugAsset extends Asset { public type = 'html'; constructor(name: string, pkg: string, options: any) { super(name, pkg, options); } public parse(code: string) { let ast = load.string(code, { lex: lexer, parse: parser, filename: this.name }); ast = linker(ast); return ast; } public collectDependencies(): void { walk(this.ast, node => { if (node.filename !== this.name && !this.dependencies.has(node.filename)) { this.addDependency(node.filename, { name: node.filename, includedInParent: true }); } if (node.attrs) { for (const attr of node.attrs) { const elements = ATTRS[attr.name]; if (node.type === 'Tag' && elements && elements.indexOf(node.name) > -1) { let assetPath = attr.val.substring(1, attr.val.length - 1); assetPath = this.addURLDependency(assetPath); if (!isURL(assetPath)) { // Use url.resolve to normalize path for windows // from \path\to\res.js to /path/to/res.js assetPath = url.resolve(path.join(this.options.publicURL, assetPath), ''); } attr.val = `'${assetPath}'`; } } } return node; }); } public generate() { const result = generateCode(this.ast, { compileDebug: false, pretty: !this.options.minify }); return { html: wrap(result)() }; } };
Our plugin is ready. He knows how to accept templates as input, convert text into AST, resolve all internal dependencies and output ready HTML at output, correctly recognize embedded and include
constructs, as well as reassemble the entire template containing these constructions.
From minor flaws - when an error occurs, its text is duplicated, which is a feature of the output of Parcel, which wraps function calls in a try catch
and prints out errors in a nice way.
Source: https://habr.com/ru/post/344858/
All Articles