📜 ⬆️ ⬇️

Parcel - write plugin


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.


Some theory


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 

How to work with Pug AST


There are several official packages for working with AST:



Plugin


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:


modules.d.ts
 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:


PugAsset.ts
 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.


Links



')

Source: https://habr.com/ru/post/344858/


All Articles