### Install Dependencies
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Run this command in the root folder to install project dependencies before starting the workshop.
```bash
yarn install
```
--------------------------------
### Example HTML with data-test attributes
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This is an example of a component invocation with attributes that need to be removed.
```handlebars
```
--------------------------------
### Initial ESLint Rule: Report All Properties
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This is the starting point for the ESLint rule, reporting all Property nodes encountered. It serves as a basic structure before applying specific checks.
```javascript
return {
Property(node) {
context.report({
node,
message: 'Unnecessary injection argument',
});
}
};
```
--------------------------------
### Ember Service Injection Example
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Demonstrates an Ember component with a service injection. The 'search' argument is unnecessary as it matches the property key.
```javascript
Component.extend({
search: service('search'),
})
```
--------------------------------
### Traverse AST for Block Statements
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This snippet shows the basic structure for traversing an AST and visiting BlockStatement nodes. It serves as a starting point for more specific checks.
```javascript
glimmer.traverse(root, {
BlockStatement(node) {
// TODO check for the other requirements and warn if they match
},
});
```
--------------------------------
### Linting Results Example
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This output shows the results of running ember-template-lint with a custom rule, indicating detected 'unless/else' errors in specific files and line numbers.
```text
$ yarn -s lint:hbs
app/components/crate-toml-copy.hbs
6:8 error Found unless/else custom/no-unless-else
app/templates/crate/owners.hbs
60:8 error Found unless/else custom/no-unless-else
✖ 2 problems (2 errors, 0 warnings)
```
--------------------------------
### Handlebars Template Example
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This is a basic Handlebars template structure. It includes a div with a title, body, and a button that triggers an 'on' element modifier.
```handlebars
{{title}}
{{body}}
```
--------------------------------
### Element Modifier with Named Parameter
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Example demonstrating the 'on' element modifier in Handlebars with a named parameter 'passive'. This shows how to pass additional options to modifiers.
```handlebars
{{on "click" this.onNext passive=true}}
```
--------------------------------
### Remove data-test-* attributes by returning null
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This AST transform visitor targets AttrNodes and removes them by returning null if their name starts with 'data-test-'. This is one method for deleting nodes from the AST.
```javascript
module.exports = function() {
return {
name: 'ast-transform',
visitor: {
AttrNode(node) {
if (node.name.startsWith('data-test-')) {
return null;
}
}
}
};
};
```
--------------------------------
### Remove data-test-* attributes by filtering ElementNode attributes
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This AST transform visitor targets ElementNodes and filters their attributes, removing any AttrNode whose name starts with 'data-test-'. This approach directly modifies the attributes array of the element.
```javascript
module.exports = function() {
return {
name: 'ast-transform',
visitor: {
ElementNode(node) {
node.attributes = node.attributes
.filter(it => !it.name.startsWith('data-test-'));
}
}
};
};
```
--------------------------------
### Parse, Modify, and Print Template AST
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Demonstrates the complete cycle of reading a template, parsing it into an AST, performing modifications (placeholder), converting the AST back to text, and writing changes back to the file.
```javascript
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = recast.parse(template);
// TODO modify the AST
// convert the AST back into text
let newTemplate = recast.print(root);
// if necessary, write the changes back to the original file
if (newTemplate !== template) {
fs.writeFileSync(templatePath, newTemplate, 'utf8')
}
}
```
--------------------------------
### Initial Script Output
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
The expected output after running the `count-tags.js` script before any tag counting logic is implemented.
```text
Map {}
```
--------------------------------
### Basic ESLint Rule Structure
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This is the initial structure for a custom ESLint rule. The `create` method is expected to return a visitor object.
```javascript
module.exports = {
create: function(context) {
// TODO write your implementation here
}
};
```
--------------------------------
### Full Script to Find Unless/Else in Templates
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This complete Node.js script reads all .hbs files in the 'app/' directory, parses them into ASTs, and traverses each AST to find and log 'unless/else' patterns. It requires 'fs', 'globby', and '@glimmer/syntax' packages.
```javascript
const fs = require('fs');
const globby = require('globby');
const glimmer = require('@glimmer/syntax');
// find all template files in the `app/` folder
let templatePaths = globby.sync('app/**/*.hbs', {
cwd: __dirname,
absolute: true,
});
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = glimmer.preprocess(template);
// use `traverse()` to "visit" all of the nodes in the AST
glimmer.traverse(root, {
BlockStatement(node) {
if (
// first we make sure that `node.path` is really a `PathExpression`
// since there are a few edge cases where it might not be
node.path.type === 'PathExpression' &&
// then we check if this is an `unless` block
node.path.original === 'unless' &&
// and finally, we check if the block has an `else` block too
node.inverse
) {
// if so, we print a warning to the console
console.log(`Found unless/else in ${templatePath}:${node.loc.start.line}:${node.loc.start.column}`);
}
},
});
}
```
--------------------------------
### Run Tag Counting Script
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Execute the Node.js script to analyze Handlebars templates and count HTML tag and component usage. The initial output shows an empty map before implementation.
```bash
node count-tags.js
```
--------------------------------
### Traverse AST Nodes
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Demonstrates how to use glimmer.traverse to visit specific AST node types like ElementNode and AttrNode. Useful for inspecting AST structure.
```javascript
glimmer.traverse(root, {
ElementNode(node) {
console.log(node);
},
AttrNode(node) {
console.log(node);
},
});
```
--------------------------------
### Swap Block Contents (Workaround)
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This code implements the workaround for swapping the contents of 'program' and 'inverse' blocks within a Handlebars AST node. It swaps the 'body' arrays of the program and inverse blocks to correctly exchange their content.
```javascript
let programBody = node.program.body;
let inverseBody = node.inverse.body;
node.program.body = inverseBody;
node.inverse.body = programBody;
```
--------------------------------
### ESLint Visitor for Call Expressions
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This snippet demonstrates how to implement a visitor for `CallExpression` nodes in ESLint. It reports a generic message for any call expression found.
```javascript
return {
CallExpression(node) {
context.report({
node,
message: 'Unexpected console.log() expression',
});
}
};
```
--------------------------------
### ESLint Rule: No Console Log
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This code implements a basic ESLint rule that reports unexpected `console.log()` expressions. It is suitable for beginners learning to create ESLint rules.
```javascript
context.report({
node,
message: 'Unexpected console.log() expression',
});
}
};
}
};
```
--------------------------------
### Count Handlebars Tag Occurrences
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Parses all .hbs files in the app directory, traverses the AST to count each ElementNode's tag, and logs the final counts. Requires globby, fs, and @glimmer/syntax.
```javascript
const fs = require('fs');
const globby = require('globby');
const glimmer = require('@glimmer/syntax');
let tagCounter = new Map();
// find all template files in the `app/` folder
let templatePaths = globby.sync('app/**/*.hbs', {
cwd: __dirname,
absolute: true,
});
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = glimmer.preprocess(template);
// use `traverse()` to "visit" all of the nodes in the AST
glimmer.traverse(root, {
ElementNode(node) {
// read the tag name from the `ElementNode`
let { tag } = node;
// read the current count for that tag name
let previousCount = tagCounter.get(tag) || 0;
// increase the counter by 1
tagCounter.set(tag, previousCount + 1);
}
});
}
// output the raw results
console.log(tagCounter);
```
--------------------------------
### Parse Template Files into AST
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Reads template files and parses their content into an Abstract Syntax Tree (AST) using ember-template-recast. This is the initial step for any AST manipulation.
```javascript
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = recast.parse(template);
}
```
--------------------------------
### Full Codemod Solution for Ember.js Templates
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This snippet finds all `.hbs` files in the `app/` directory, parses their content, traverses the AST to rename `MenuItem` components to `NewMenuItem`, moves the `@caption` attribute value to children, removes the `@caption` attribute, and writes the modified template back to the file if changes were made. It requires `fs`, `globby`, and `ember-template-recast`.
```javascript
const fs = require('fs');
const globby = require('globby');
const recast = require('ember-template-recast');
// find all template files in the `app/` folder
let templatePaths = globby.sync('app/**/*.hbs', {
cwd: __dirname,
absolute: true,
});
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = recast.parse(template);
// use `traverse()` to "visit" all of the nodes in the AST
recast.traverse(root, {
ElementNode(node) {
// filter out non-MenuItem elements
if (node.tag !== 'MenuItem') return;
// change the tag name to `NewMenuItem`
node.tag = 'NewMenuItem';
// find the `@caption` attribute
let captionAttr = node.attributes.find(it => it.name === '@caption');
if (captionAttr) {
// move the `@caption` value into the element's `children`
node.children = [captionAttr.value];
}
// remove `@caption` attribute
node.attributes = node.attributes.filter(it => it.name !== '@caption');
}
});
// convert the AST back into text
let newTemplate = recast.print(root);
// if necessary, write the changes back to the original file
if (newTemplate !== template) {
fs.writeFileSync(templatePath, newTemplate, 'utf8')
}
}
```
--------------------------------
### Identify Unless Blocks with Else
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This visitor function identifies 'unless' block statements that also have an 'inverse' (else) block. It ensures the node path is a PathExpression before checking the original name and the presence of an inverse block.
```javascript
recast.traverse(root, {
BlockStatement(node) {
if (
// first we make sure that `node.path` is really a `PathExpression`
// since there are a few edge cases where it might not be
node.path.type === 'PathExpression' &&
// then we check if this is an `unless` block
node.path.original === 'unless' &&
// and finally, we check if the block has an `else` block too
node.inverse
) {
// TODO transform this node
}
}
});
```
--------------------------------
### Full Codemod for Unless/Else Transformation
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This script finds all Handlebars template files, parses them, traverses the AST to find 'unless' blocks with 'else' clauses, swaps their contents, changes 'unless' to 'if', and writes the modified templates back. It includes necessary imports for file system operations, globbing, and ember-template-recast.
```javascript
const fs = require('fs');
const globby = require('globby');
const recast = require('ember-template-recast');
// find all template files in the `app/` folder
let templatePaths = globby.sync('app/**/*.hbs', {
cwd: __dirname,
absolute: true,
});
for (let templatePath of templatePaths) {
// read the file content
let template = fs.readFileSync(templatePath, 'utf8');
// parse the file content into an AST
let root = recast.parse(template);
// use `traverse()` to "visit" all of the nodes in the AST
recast.traverse(root, {
BlockStatement(node) {
if (
// first we make sure that `node.path` is really a `PathExpression`
// since there are a few edge cases where it might not be
node.path.type === 'PathExpression' &&
// then we check if this is an `unless` block
node.path.original === 'unless' &&
// and finally, we check if the block has an `else` block too
node.inverse
) {
let { program, inverse } = node;
let programBody = program.body;
let inverseBody = inverse.body;
// swap `program` and `inverse` blocks
node.program.body = inverseBody;
node.inverse.body = programBody;
// change the block statement from `unless` to `if`
node.path.original = 'if';
}
}
});
// convert the AST back into text
let newTemplate = recast.print(root);
// if necessary, write the changes back to the original file
if (newTemplate !== template) {
fs.writeFileSync(templatePath, newTemplate, 'utf8')
}
}
```
--------------------------------
### AST Transformation: Collapse Whitespace in TextNodes
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This AST plugin targets TextNodes and replaces collapsible whitespace within their 'chars' attribute with a single space. It's designed for use with the AST Explorer's glimmer transform.
```javascript
module.exports = function() {
return {
name: 'ast-transform',
visitor: {
TextNode(node) {
node.chars = node.chars.replace(/[ \r\n]+/g, ' ');
}
}
};
};
```
--------------------------------
### ESLint Rule: Check Property and CallExpression Types
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Refines the ESLint rule by checking if the Property's key is an Identifier and its value is a CallExpression. This narrows down the potential candidates for unnecessary arguments.
```javascript
let { key, value } = node;
if (key.type !== 'Identifier') return;
if (value.type !== 'CallExpression') return;
```
--------------------------------
### ESLint Rule: Check Service/Inject Callee
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Further refines the ESLint rule by verifying that the CallExpression's callee is an Identifier named 'service' or 'inject'. This ensures we are targeting the correct Ember injection patterns.
```javascript
let { callee } = value;
if (callee.type !== 'Identifier') return;
if (!['inject', 'service'].includes(callee.name)) return;
```
--------------------------------
### ESLint Rule: Report Unnecessary Argument
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Once all conditions are met, this code instructs ESLint to report the unnecessary injection argument, highlighting the specific Literal node.
```javascript
context.report({
node: arg,
message: 'Unnecessary injection argument',
});
```
--------------------------------
### Remove Test Selectors from HTML
Source: https://github.com/mainmatter/ast-workshop/blob/master/05b-strip-test-selectors/tests/index.html
Use AST to parse HTML and remove elements with data-testid attributes.
```javascript
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
const html = `
Some text
User Name
`;
const ast = parse(html, {
sourceType: "module",
plugins: ["jsx"],
});
traverse(ast, {
enter(path) {
if (
path.isJSXOpeningElement() &&
path.node.attributes.some(
(attr) => attr.type === "JSXAttribute" && attr.name.name === "data-testid"
)
) {
path.remove();
}
},
});
const { code } = generate(ast);
console.log(code);
```
--------------------------------
### Change Unless to If
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This snippet demonstrates how to change an 'unless' block statement to an 'if' statement by modifying the 'original' property of the node's path.
```javascript
node.path.original = 'if';
```
--------------------------------
### Refining ESLint Rule for console.log
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This code refines the ESLint rule to specifically target `console.log()` calls by checking the AST node types and names.
```javascript
module.exports = {
create: function(context) {
return {
CallExpression(node) {
let { callee } = node;
if (callee.type !== 'MemberExpression') return;
let { object, property } = callee;
if (object.type !== 'Identifier' || object.name !== 'console') return;
if (property.type !== 'Identifier' || property.name !== 'log') return;
}
};
}
};
```
--------------------------------
### Collapse Whitespace with Regex
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Use this regular expression to replace all collapsible whitespace characters (spaces, newlines, carriage returns, tabs) with a single space. The 'g' flag ensures all occurrences are replaced.
```javascript
let newText = oldText.replace(/[ \n\r\t]+/g, ' ');
```
--------------------------------
### Custom Ember Template Lint Rule for No Unless Else
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This JavaScript code defines a custom ember-template-lint rule. It uses the visitor pattern to detect and report 'unless' blocks that also have an 'else' block.
```javascript
const Rule = require('ember-template-lint').Rule;
module.exports = class extends Rule {
visitor() {
return {
BlockStatement(node) {
if (
// first we make sure that `node.path` is really a `PathExpression`
// since there are a few edge cases where it might not be
node.path.type === 'PathExpression' &&
// then we check if this is an `unless` block
node.path.original === 'unless' &&
// and finally, we check if the block has an `else` block too
node.inverse
) {
// if so, report a template-lint warning
this.log({
message: 'Found unless/else',
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node)
});
}
},
};
}
};
```
--------------------------------
### Check for Unless Block with Else
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This code segment checks if a given BlockStatement node represents an 'unless' block and if it has an associated 'else' block (inverse attribute is not null).
```javascript
if (
// first we make sure that `node.path` is really a `PathExpression`
// since there are a few edge cases where it might not be
node.path.type === 'PathExpression' &&
// then we check if this is an `unless` block
node.path.original === 'unless' &&
// and finally, we check if the block has an `else` block too
node.inverse
) {
// TODO print warning
}
```
--------------------------------
### ESLint Rule: Check Literal Argument Value
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
This part of the ESLint rule checks if the first argument of the CallExpression is a Literal and if its value matches the name of the Property key. This is the final condition to identify an unnecessary injection argument.
```javascript
let arg = value.arguments[0];
if (!arg) return;
if (arg.type !== 'Literal') return;
if (arg.value !== key.name) return;
```
--------------------------------
### Traverse AST to Modify Element Tag Names
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Uses the `traverse` function to find all `ElementNode`s in the AST and modifies their tag names. This snippet specifically targets and renames 'MenuItem' components to 'NewMenuItem'.
```javascript
recast.traverse(root, {
ElementNode(node) {
// filter out non-MenuItem elements
if (node.tag !== 'MenuItem') return;
// change the tag name to `NewMenuItem`
node.tag = 'NewMenuItem';
},
});
```
--------------------------------
### Find and Move Caption Attribute Value
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Locates the '@caption' attribute within an `ElementNode`'s attributes and moves its value to the `children` array of the `ElementNode`. This transforms an attribute into a child element.
```javascript
// find the `@caption` attribute
let captionAttr = node.attributes.find(it => it.name === '@caption');
if (captionAttr) {
// move the `@caption` value into the element's `children`
node.children = [captionAttr.value];
}
```
--------------------------------
### Remove Caption Attribute
Source: https://github.com/mainmatter/ast-workshop/blob/master/README.md
Filters the `attributes` array of an `ElementNode` to remove the '@caption' attribute. This is done after its value has been moved to the `children` array to avoid duplication.
```javascript
// remove `@caption` attribute
node.attributes = node.attributes.filter(it => it.name !== '@caption');
```
=== COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.